Compare commits

...
Author SHA1 Message Date
Sungun Park 55dbafe0b0 gl: Prevent crashes on shader compilation/link failure
Modified ShaderCompilerService to gracefully handle shader compilation
failures without crashing.

Previously, a failed shader compilation could lead to a crash. Now, it
identifies the failure, sets a `compilationFailed` flag, and skips
subsequent operations like program linking. This prevents the
application from crashing and avoids printing unnecessary warning
messages for materials that will not be rendered.

Additionally, the logic within the runAtNextTick lambda in the
createProgram function has been refactored for improved readability and
maintainability.

BUGS=[373396840]
2025-07-07 17:17:26 -07:00
rafadevaiandSerge Metral 4879a0a124 VK: Fix calculating the alignment for updateImage (#8929)
The alignment must be calculated using the
VulkanTexture format (dstImage) instead of the
PixelBufferDescriptor format.

Set the correct values for all supported formats
in getTexelBlockSize.

Use a helper function to correctly calculate the
required aligment based on the spec rules for
vkCmdCopyBufferToImage.

Co-authored-by: Serge Metral <sergemetral@google.com>
2025-07-07 10:59:05 -07:00
Mathias Agopian 637affefeb undeprecate a few methods because there is no cost keeping them
Box::transform is just an inline
IndirectLight::get{Color|Direction}Estimate() can be document to handle
the UB cases.
2025-07-07 10:10:12 -07:00
Andy Hovingh 8d219d3bc0 webgpu: render pipeline cache 2025-07-07 08:35:42 -05:00
Konrad Piascik a7289da05f webgpu: remove unnecessary include to avoid iOS compilation failure 2025-07-04 10:27:12 -04:00
Powei Feng 98eea205f3 renderdiff: viewer can pull artifacts given PR number (#8919)
This will allow for a faster flow then downloading manually.
Though it will require user to supply a github token.
2025-07-03 14:17:31 -07:00
Andy Hovingh 1c22b48893 webgpu: does not support depth/stencil resolve at this time 2025-07-03 10:48:43 -05:00
Mathias Agopian 249dd9752d Fix SSR when postfx and msaa are disabled.
The problem was that we need to know before rendering if we're doing so
directly in the swapchain or an intermediate buffer. Unfortunately 
this is determined by the framegraph (after culling and such). But at
the point we need it the framefraph is not even constructed.

So we need to "guess" this from the parameters of the View. That logic
missed the case where SSR (reflection or refraction) was used.

An extra level of complexity is that we need to generate the high level
commands before we can determine if refraction will happen.

Fix #8910
2025-07-02 16:58:11 -07:00
Anish GoyalandSerge Metral bb4cd43835 Convert staging images to resources (#8834)
* Convert staging images to resources

This will reduce the amount of computation required in gc() calls to
determine if a staging image is still in use or not. This follows the
change made to staging buffers.

* Address PR comment: formatting

Split up >100 char lines, runs clang-format on changed lines

---------

Co-authored-by: Serge Metral <sergemetral@google.com>
2025-07-02 12:52:26 -07:00
Sungun Park 954d1021c7 Add #include <cstdlib> to avoid compilation error (#8922) 2025-07-02 18:54:03 +00:00
Sungun Park bf598071a3 gl: move shader code instead of copy (#8918)
In Debug mode, remove unnecessary memory reallocation & copy by moving
shader code blob.
2025-07-02 17:41:43 +00:00
Powei Feng 5cbc2321c7 Add feature flag for attribute stride check (#8913)
We also add a define to log (instead of assert) failtures of
certain correctness checks.  This allows clients to gradually
fix their code.
2025-07-02 17:06:37 +00:00
Mathias Agopian 6bee5fb56f fix PixelBufferDescriptor overflow calculation
We simply compute the coordinate of the last pixel that need to be 
accessed and deduce its offset, which must be smaller than the 
buffer size.
2025-07-01 16:08:58 -07:00
Mathias Agopian eb0fcc065b workaround gcc permissive warning. Fixes #8883. 2025-07-01 16:08:26 -07:00
Andy Hovingh 117535364b webgpu: support auto-resolve render passes (render target samples != attached texture samples)
The solution resembles that of the other backends, to implement msaa sidecar textures.

Note: this solution does not resolve depth/stencil textures
2025-07-01 15:59:09 -05:00
Powei Feng b15ae15f39 No need for blending when upscaling (#8900)
The dst buffer is allocated and should not be blended against.
The blending code is a relic of a previous code path that's
no longer present [link].

[link]: https://github.com/google/filament/blame/c049a1bfff0851098bf640159fb6c5daab88d5d4/filament/src/PostProcessManager.cpp

FIXES=422593171
2025-07-01 05:43:23 +00:00
47 changed files with 2162 additions and 927 deletions
+6 -2
View File
@@ -273,8 +273,12 @@ if (FILAMENT_SUPPORTS_WEBGPU)
src/webgpu/WebGPUFence.h
src/webgpu/WebGPUIndexBuffer.cpp
src/webgpu/WebGPUIndexBuffer.h
src/webgpu/WebGPUPipelineCreation.cpp
src/webgpu/WebGPUPipelineCreation.h
src/webgpu/WebGPUMsaaTextureResolver.cpp
src/webgpu/WebGPUMsaaTextureResolver.h
src/webgpu/WebGPUPipelineCache.cpp
src/webgpu/WebGPUPipelineCache.h
src/webgpu/WebGPUPipelineLayoutCache.cpp
src/webgpu/WebGPUPipelineLayoutCache.h
src/webgpu/WebGPUProgram.cpp
src/webgpu/WebGPUProgram.h
src/webgpu/WebGPURenderPassMipmapGenerator.cpp
@@ -66,7 +66,7 @@ static std::string to_string(int const i) { return std::to_string(i); }
static std::string to_string(float const f) { return "float(" + std::to_string(f) + ")"; }
static void logCompilationError(ShaderStage shaderType, const char* name, GLuint shaderId,
CString const& sourceCode) noexcept;
Program::ShaderBlob const& sourceCode) noexcept;
static void logProgramLinkError(char const* name, GLuint program) noexcept;
static void process_GOOGLE_cpp_style_line_directive(OpenGLContext const& context, char* source,
@@ -88,7 +88,7 @@ struct ShaderCompilerService::OpenGLProgramToken : ProgramToken {
ShaderCompilerService& compiler;
CString const& name;
FixedCapacityVector<std::pair<CString, uint8_t>> attributes;
shaders_source_t shaderSourceCode;
Program::ShaderSource shaderSourceCode;
void* user = nullptr;
struct {
shaders_t shaders{};
@@ -121,6 +121,12 @@ struct ShaderCompilerService::OpenGLProgramToken : ProgramToken {
}
}
// Checks the token's completion status. The token is considered ready if the program was
// created successfully or if a shader compilation error occurred.
bool isReady() const noexcept {
return gl.program || compilationFailed;
}
std::optional<CallbackManager::Handle> handle{};
// Only valid when the blob functions are provided by users. The validity of this variable
@@ -134,6 +140,9 @@ struct ShaderCompilerService::OpenGLProgramToken : ProgramToken {
// Indicate this program was created from the cache blob.
bool retrievedFromBlobCache = false;
// Indicates that shader compilation failed.
bool compilationFailed = false;
};
ShaderCompilerService::OpenGLProgramToken::~OpenGLProgramToken() {
@@ -297,22 +306,33 @@ ShaderCompilerService::program_token_t ShaderCompilerService::createProgram(
runAtNextTick(priorityQueue, token, [this, token](Job const&) {
assert_invariant(mMode != Mode::THREAD_POOL);
if (mMode == Mode::ASYNCHRONOUS) {
// Check link completion if link was initiated.
if (token->gl.program) {
if (token->gl.program) {
// Program linking has been initiated.
if (mMode == Mode::ASYNCHRONOUS) {
return isLinkCompleted(token);
}
// Link hasn't been initiated, then check compile completion.
return true; // In sync mode, if program exists, we're done with this job.
}
// Program not linked yet. Check if shaders are compiled.
if (mMode == Mode::ASYNCHRONOUS) {
if (!isCompileCompleted(token)) {
return false;
return false; // Wait for compilation to finish.
}
}
if (!token->gl.program) {
linkProgram(mDriver.getContext(), token);
if (mMode == Mode::ASYNCHRONOUS) {
return false;// Wait until the link finishes.
}
// Shaders are compiled (or we're in sync mode). Let's link.
if (!linkProgram(mDriver.getContext(), token)) {
// Shader compilation failed. Stop processing this program.
return true;
}
// Program is now linking.
if (mMode == Mode::ASYNCHRONOUS) {
return false; // Wait for linking to finish.
}
return true;
});
break;
@@ -394,6 +414,14 @@ GLuint ShaderCompilerService::initialize(program_token_t& token) {
assert_invariant(token);// This function should be called when the token is still alive.
ensureTokenIsReady(token);
if (token->compilationFailed) {
// Cleanup the token.
token->compiler.cancelTickOp(token);
token = nullptr;
return 0;
}
assert_invariant(token->gl.program);
// Check status of program linking. If it failed, errors will be logged.
@@ -419,7 +447,7 @@ GLuint ShaderCompilerService::initialize(program_token_t& token) {
}
void ShaderCompilerService::ensureTokenIsReady(program_token_t const& token) {
if (token->gl.program) {
if (token->isReady()) {
return;// It's ready.
}
@@ -612,7 +640,7 @@ void ShaderCompilerService::executeTickOps() noexcept {
#ifndef NDEBUG
// for debugging we return the original shader source (without the modifications we
// made here), otherwise the line numbers wouldn't match.
token->shaderSourceCode[i] = { shader_src, shader_len };
token->shaderSourceCode[i] = std::move(shader);
#endif
token->gl.shaders[i] = shaderId;
}
@@ -638,9 +666,10 @@ void ShaderCompilerService::executeTickOps() noexcept {
return true;
}
/* static */ void ShaderCompilerService::checkCompileStatus(program_token_t const& token) noexcept {
/* static */ bool ShaderCompilerService::checkCompileStatus(program_token_t const& token) noexcept {
FILAMENT_TRACING_CALL(FILAMENT_TRACING_CATEGORY_FILAMENT);
bool success = true;
UTILS_NOUNROLL
for (size_t i = 0; i < Program::SHADER_TYPE_COUNT; i++) {
const GLuint shader = token->gl.shaders[i];
@@ -656,15 +685,21 @@ void ShaderCompilerService::executeTickOps() noexcept {
// Something went wrong. Log the error message.
const ShaderStage type = static_cast<ShaderStage>(i);
logCompilationError(type, token->name.c_str_safe(), shader, token->shaderSourceCode[i]);
success = false;
}
return success;
}
/* static */ void ShaderCompilerService::linkProgram(OpenGLContext const& context,
/* static */ bool ShaderCompilerService::linkProgram(OpenGLContext const& context,
program_token_t const& token) noexcept {
FILAMENT_TRACING_CALL(FILAMENT_TRACING_CATEGORY_FILAMENT);
// Shader compilation should be completed by now. Check the status and log errors on failure.
checkCompileStatus(token);
if (!checkCompileStatus(token)) {
token->compilationFailed = true;
token->trySubmittingCallback();
return false;
}
// Link program
GLuint const program = glCreateProgram();
@@ -681,6 +716,7 @@ void ShaderCompilerService::executeTickOps() noexcept {
glLinkProgram(program);
token->gl.program = program;
token->trySubmittingCallback();
return true;
}
/* static */ bool ShaderCompilerService::isLinkCompleted(program_token_t const& token) noexcept {
@@ -758,7 +794,8 @@ void ShaderCompilerService::executeTickOps() noexcept {
UTILS_NOINLINE
/* static */ void logCompilationError(ShaderStage shaderType, const char* name,
GLuint const shaderId, UTILS_UNUSED_IN_RELEASE CString const& sourceCode) noexcept {
GLuint const shaderId,
UTILS_UNUSED_IN_RELEASE Program::ShaderBlob const& sourceCode) noexcept {
{ // scope for the temporary string storage
auto to_string = [](ShaderStage type) -> const char* {
@@ -785,7 +822,8 @@ UTILS_NOINLINE
}
#ifndef NDEBUG
std::string_view const shader{ sourceCode.data(), sourceCode.size() };
std::string_view const shader{ reinterpret_cast<const char*>(sourceCode.data()),
sourceCode.size() };
size_t lc = 1;
size_t start = 0;
std::string line;
@@ -58,7 +58,6 @@ class ShaderCompilerService {
public:
using program_token_t = std::shared_ptr<OpenGLProgramToken>;
using shaders_t = std::array<GLuint, Program::SHADER_TYPE_COUNT>;
using shaders_source_t = std::array<utils::CString, Program::SHADER_TYPE_COUNT>;
explicit ShaderCompilerService(OpenGLDriver& driver);
@@ -156,13 +155,14 @@ private:
static bool isCompileCompleted(program_token_t const& token) noexcept;
// Check compilation status of the shaders and log errors on failure.
static void checkCompileStatus(program_token_t const& token) noexcept;
static bool checkCompileStatus(program_token_t const& token) noexcept;
// Create a program by linking the compiled shaders. `gl.program` is always populated with a
// valid program ID after this method. But this doesn't necessarily mean the program is
// successfully linked. Errors can be checked by calling `checkLinkStatusAndCleanupShaders`
// later.
static void linkProgram(OpenGLContext const& context, program_token_t const& token) noexcept;
// Create a program by linking the compiled shaders. If the previous shader compilation was
// failed, `compilationFailed` is set, then this function returns false. Otherwise `gl.program`
// is populated with a valid program ID, then returns true. However this doesn't necessarily
// mean the program is successfully linked. The link error can be checked by calling
// `checkLinkStatusAndCleanupShaders` later.
static bool linkProgram(OpenGLContext const& context, program_token_t const& token) noexcept;
// Check if the program link is completed. You may want to call this when the extension
// `KHR_parallel_shader_compile` is enabled.
+29 -40
View File
@@ -156,27 +156,27 @@ void VulkanStagePool::destroyStage(VulkanStage const*&& stage) {
delete stage;
}
VulkanStageImage const* VulkanStagePool::acquireImage(PixelDataFormat format, PixelDataType type,
uint32_t width, uint32_t height) {
fvkmemory::resource_ptr<VulkanStageImage::Resource> VulkanStagePool::acquireImage(
PixelDataFormat format, PixelDataType type, uint32_t width, uint32_t height) {
// Helper lambda so we can return stage images wrapped as resources that can
// be held by command buffers until no longer needed.
auto wrapAsResource = [this](VulkanStageImage* image) {
auto recycleFn = [this](VulkanStageImage* image) {
this->mFreeImages.insert(image);
};
return fvkmemory::resource_ptr<VulkanStageImage::Resource>::construct(
this->mResManager, image, recycleFn);
};
const VkFormat vkformat = fvkutils::getVkFormat(format, type);
for (auto image : mFreeImages) {
if (image->format == vkformat && image->width == width && image->height == height) {
mFreeImages.erase(image);
image->lastAccessed = mCurrentFrame;
mUsedImages.push_back(image);
return image;
for (auto stageImage : mFreeImages) {
if (stageImage->format() == vkformat && stageImage->width() == width && stageImage->height() == height) {
mFreeImages.erase(stageImage);
stageImage->mLastAccessed = mCurrentFrame;
return wrapAsResource(stageImage);
}
}
VulkanStageImage* image = new VulkanStageImage({
.format = vkformat,
.width = width,
.height = height,
.lastAccessed = mCurrentFrame,
});
mUsedImages.push_back(image);
const VkImageCreateInfo imageInfo = {
.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
.imageType = VK_IMAGE_TYPE_2D,
@@ -194,14 +194,19 @@ VulkanStageImage const* VulkanStagePool::acquireImage(PixelDataFormat format, Pi
.usage = VMA_MEMORY_USAGE_CPU_TO_GPU
};
VkImage image;
VmaAllocation memory;
const UTILS_UNUSED VkResult result = vmaCreateImage(mAllocator, &imageInfo, &allocInfo,
&image->image, &image->memory, nullptr);
&image, &memory, nullptr);
assert_invariant(result == VK_SUCCESS);
VkImageAspectFlags const aspectFlags = fvkutils::getImageAspect(vkformat);
VkCommandBuffer const cmdbuffer = mCommands->get().buffer();
VulkanStageImage* stageImage = new VulkanStageImage(
vkformat, width, height, memory, image, mCurrentFrame);
// We use VK_IMAGE_LAYOUT_GENERAL here because the spec says:
// "Host access to image memory is only well-defined for linear images and for image
// subresources of those images which are currently in either the
@@ -209,12 +214,13 @@ VulkanStageImage const* VulkanStagePool::acquireImage(PixelDataFormat format, Pi
// vkGetImageSubresourceLayout for a linear image returns a subresource layout mapping that is
// valid for either of those image layouts."
fvkutils::transitionLayout(cmdbuffer, {
.image = image->image,
.image = stageImage->image(),
.oldLayout = VulkanLayout::UNDEFINED,
.newLayout = VulkanLayout::STAGING, // (= VK_IMAGE_LAYOUT_GENERAL)
.subresources = { aspectFlags, 0, 1, 0, 1 },
});
return image;
return wrapAsResource(stageImage);
}
void VulkanStagePool::gc() noexcept {
@@ -262,25 +268,14 @@ void VulkanStagePool::gc() noexcept {
decltype(mFreeImages) freeImages;
freeImages.swap(mFreeImages);
for (auto image : freeImages) {
if (image->lastAccessed < evictionTime) {
vmaDestroyImage(mAllocator, image->image, image->memory);
if (image->mLastAccessed < evictionTime) {
vmaDestroyImage(mAllocator, image->image(), image->memory());
delete image;
} else {
mFreeImages.insert(image);
}
}
// Reclaim images that are no longer being used by any command buffer.
decltype(mUsedImages) usedImages;
usedImages.swap(mUsedImages);
for (auto image : usedImages) {
if (image->lastAccessed < evictionTime) {
image->lastAccessed = mCurrentFrame;
mFreeImages.insert(image);
} else {
mUsedImages.push_back(image);
}
}
FVK_SYSTRACE_END();
}
@@ -290,14 +285,8 @@ void VulkanStagePool::terminate() noexcept {
}
mStages.clear();
for (auto image : mUsedImages) {
vmaDestroyImage(mAllocator, image->image, image->memory);
delete image;
}
mUsedImages.clear();
for (auto image : mFreeImages) {
vmaDestroyImage(mAllocator, image->image, image->memory);
vmaDestroyImage(mAllocator, image->image(), image->memory());
delete image;
}
mFreeImages.clear();
+66 -10
View File
@@ -126,13 +126,70 @@ private:
std::unordered_map<uint32_t, Segment*> mSegments;
};
struct VulkanStageImage {
VkFormat format;
uint32_t width;
uint32_t height;
mutable uint64_t lastAccessed;
VmaAllocation memory;
VkImage image;
class VulkanStageImage {
public:
class Resource : public fvkmemory::Resource {
public:
using RecycleFn = std::function<void(VulkanStageImage*)>;
Resource(VulkanStageImage* image, RecycleFn&& onRecycleFn)
: mImage(image),
mOnRecycleFn(onRecycleFn)
{}
~Resource() {
if (mOnRecycleFn) {
mOnRecycleFn(mImage);
}
}
inline VkFormat format() const { return mImage->format(); }
inline uint32_t width() const { return mImage->width(); }
inline uint32_t height() const { return mImage->height(); }
inline VmaAllocation memory() const { return mImage->memory(); }
inline VkImage image() const { return mImage->image(); }
private:
Resource() = delete;
Resource(const Resource& other) = delete;
Resource(Resource&& other) = delete;
Resource& operator=(const Resource& other) = delete;
Resource& operator=(Resource&& other) = delete;
VulkanStageImage* const mImage;
RecycleFn mOnRecycleFn;
};
VulkanStageImage(VkFormat format, uint32_t width, uint32_t height, VmaAllocation memory,
VkImage image, uint64_t lastAccessed)
: mFormat(format),
mWidth(width),
mHeight(height),
mMemory(memory),
mImage(image),
mLastAccessed(lastAccessed) {}
VulkanStageImage(const VulkanStageImage& other) = delete;
VulkanStageImage(VulkanStageImage&& other) = delete;
VulkanStageImage& operator=(const VulkanStageImage& other) = delete;
VulkanStageImage& operator=(VulkanStageImage&& other) = delete;
inline VkFormat format() const { return mFormat; }
inline uint32_t width() const { return mWidth; }
inline uint32_t height() const { return mHeight; }
inline VmaAllocation memory() const { return mMemory; }
inline VkImage image() const { return mImage; }
private:
const VkFormat mFormat;
const uint32_t mWidth;
const uint32_t mHeight;
const VmaAllocation mMemory;
const VkImage mImage;
uint64_t mLastAccessed;
// Denote as a friend so that it can update mLastAccessed.
friend class VulkanStagePool;
};
// Manages a pool of stages, periodically releasing stages that have been unused for a while.
@@ -153,7 +210,7 @@ public:
uint32_t alignment = 0);
// Images have VK_IMAGE_LAYOUT_GENERAL and must not be transitioned to any other layout
VulkanStageImage const* acquireImage(PixelDataFormat format, PixelDataType type,
fvkmemory::resource_ptr<VulkanStageImage::Resource> acquireImage(PixelDataFormat format, PixelDataType type,
uint32_t width, uint32_t height);
// Evicts old unused stages and bumps the current frame number.
@@ -193,8 +250,7 @@ private:
// Use an ordered multimap for quick (capacity => stage) lookups using lower_bound().
std::multimap<uint32_t, VulkanStage*> mStages;
std::unordered_set<VulkanStageImage const*> mFreeImages;
std::vector<VulkanStageImage const*> mUsedImages;
std::unordered_set<VulkanStageImage*> mFreeImages;
// Store the current "time" (really just a frame count) and LRU eviction parameters.
uint64_t mCurrentFrame = 0;
+22 -7
View File
@@ -255,6 +255,21 @@ void adjustedMemcpy(void* mapped, PixelBufferDescriptor const& p, size_t width,
}
}
uint8_t getAlignmentForBufferToImageCopy(VkFormat format) {
// VUID-vkCmdCopyBufferToImage-dstImage-07978
if(fvkutils::isVkDepthFormat(format) || fvkutils::isVkStencilFormat(format)) {
return 4;
}
if (fvkutils::isVKYcbcrConversionFormat(format)) {
assert_invariant(false && "Multi planar format is not supported");
return 1;
}
// VUID-vkCmdCopyBufferToImage-dstImage-07975
return fvkutils::getTexelBlockSize(format);
}
} // anonymous namespace
VulkanTextureState::VulkanTextureState(VulkanStagePool& stagePool, VulkanCommands* commands,
@@ -526,8 +541,7 @@ void VulkanTexture::updateImage(const PixelBufferDescriptor& data, uint32_t widt
// Note: the following stageSegment must be stored within the command buffer
// before going out of scope, to ensure proper bookkeeping within the
// staging buffer pool.
uint8_t alignment =
fvkutils::getTexelBlockSize(fvkutils::getVkFormat(hostData->format, hostData->type));
uint8_t alignment = getAlignmentForBufferToImageCopy(mState->mVkFormat);
fvkmemory::resource_ptr<VulkanStage::Segment> stageSegment =
mState->mStagePool.acquireStage(writeSize, alignment);
assert_invariant(stageSegment->memory());
@@ -602,15 +616,16 @@ void VulkanTexture::updateImageWithBlit(const PixelBufferDescriptor& data, uint3
size_t const writeSize = bpp > 0 ? width * height * depth * bpp : data.size;
void* mapped = nullptr;
VulkanStageImage const* stage
fvkmemory::resource_ptr<VulkanStageImage::Resource> stage
= mState->mStagePool.acquireImage(data.format, data.type, width, height);
vmaMapMemory(mState->mAllocator, stage->memory, &mapped);
vmaMapMemory(mState->mAllocator, stage->memory(), &mapped);
adjustedMemcpy(mapped, data, width, height, depth);
vmaUnmapMemory(mState->mAllocator, stage->memory);
vmaFlushAllocation(mState->mAllocator, stage->memory, 0, writeSize);
vmaUnmapMemory(mState->mAllocator, stage->memory());
vmaFlushAllocation(mState->mAllocator, stage->memory(), 0, writeSize);
VulkanCommandBuffer& commands = mState->mCommands->get();
VkCommandBuffer const cmdbuf = commands.buffer();
commands.acquire(stage);
commands.acquire(fvkmemory::resource_ptr<VulkanTexture>::cast(this));
// TODO: support blit-based format conversion for 3D images and cubemaps.
@@ -633,7 +648,7 @@ void VulkanTexture::updateImageWithBlit(const PixelBufferDescriptor& data, uint3
VulkanLayout const oldLayout = getLayout(layer, miplevel);
transitionLayout(&commands, range, newLayout);
vkCmdBlitImage(cmdbuf, stage->image, fvkutils::getVkLayout(VulkanLayout::TRANSFER_SRC),
vkCmdBlitImage(cmdbuf, stage->image(), fvkutils::getVkLayout(VulkanLayout::TRANSFER_SRC),
mState->mTextureImage, fvkutils::getVkLayout(newLayout), 1, blitRegions, VK_FILTER_NEAREST);
transitionLayout(&commands, range, oldLayout);
@@ -27,6 +27,7 @@ template ResourceType getTypeEnum<VulkanProgram>() noexcept;
template ResourceType getTypeEnum<VulkanRenderTarget>() noexcept;
template ResourceType getTypeEnum<VulkanSwapChain>() noexcept;
template ResourceType getTypeEnum<VulkanStage::Segment>() noexcept;
template ResourceType getTypeEnum<VulkanStageImage::Resource>() noexcept;
template ResourceType getTypeEnum<VulkanRenderPrimitive>() noexcept;
template ResourceType getTypeEnum<VulkanTexture>() noexcept;
template ResourceType getTypeEnum<VulkanTextureState>() noexcept;
@@ -58,6 +59,9 @@ ResourceType getTypeEnum() noexcept {
if constexpr (std::is_same_v<D, VulkanStage::Segment>) {
return ResourceType::STAGE_SEGMENT;
}
if constexpr (std::is_same_v<D, VulkanStageImage::Resource>) {
return ResourceType::STAGE_IMAGE;
}
if constexpr (std::is_same_v<D, VulkanRenderPrimitive>) {
return ResourceType::RENDER_PRIMITIVE;
}
@@ -105,6 +109,8 @@ std::string getTypeStr(ResourceType type) {
return "SwapChain";
case ResourceType::STAGE_SEGMENT:
return "Stage::Segment";
case ResourceType::STAGE_IMAGE:
return "Stage::Image";
case ResourceType::RENDER_PRIMITIVE:
return "RenderPrimitive";
case ResourceType::TEXTURE:
@@ -51,7 +51,8 @@ enum class ResourceType : uint8_t {
FENCE = 13,
VULKAN_BUFFER = 14,
STAGE_SEGMENT = 15,
UNDEFINED_TYPE = 16, // Must be the last enum because we use it for iterating over the enums.
STAGE_IMAGE = 16,
UNDEFINED_TYPE = 17, // Must be the last enum because we use it for iterating over the enums.
};
template<typename D>
@@ -81,6 +81,9 @@ void ResourceManager::destroyWithType(ResourceType type, HandleId id) {
case ResourceType::STAGE_SEGMENT:
destruct<VulkanStage::Segment>(Handle<VulkanStage::Segment>(id));
break;
case ResourceType::STAGE_IMAGE:
destruct<VulkanStageImage::Resource>(Handle<VulkanStageImage::Resource>(id));
break;
case ResourceType::RENDER_PRIMITIVE:
destruct<VulkanRenderPrimitive>(Handle<VulkanRenderPrimitive>(id));
break;
+161 -69
View File
@@ -233,15 +233,12 @@ VkFormat getVkFormat(TextureFormat format) {
}
}
// As per VUID-vkCmdCopyBufferToImage-dstImage-07975 and
// VUID-vkCmdCopyBufferToImage-dstImage-07978, these provide the texel block
// sizes for each format (as confirmed using the table at
// As per
// https://registry.khronos.org/vulkan/specs/latest/html/vkspec.html#formats-compatibility-classes).
// We have not listed values for multi-plane formats, as we do not support them
// unless they're externally provided.
uint8_t getTexelBlockSize(VkFormat format) {
switch (format) {
// 8-bit formats.
case VK_FORMAT_R4G4_UNORM_PACK8:
case VK_FORMAT_R8_UNORM:
case VK_FORMAT_R8_SNORM:
case VK_FORMAT_R8_USCALED:
@@ -249,16 +246,21 @@ uint8_t getTexelBlockSize(VkFormat format) {
case VK_FORMAT_R8_UINT:
case VK_FORMAT_R8_SINT:
case VK_FORMAT_R8_SRGB:
case VK_FORMAT_S8_UINT:
return 1;
// 16-bit formats.
case VK_FORMAT_R16_UNORM:
case VK_FORMAT_R16_SNORM:
case VK_FORMAT_R16_USCALED:
case VK_FORMAT_R16_SSCALED:
case VK_FORMAT_R16_UINT:
case VK_FORMAT_R16_SINT:
case VK_FORMAT_R16_SFLOAT:
case VK_FORMAT_R10X6_UNORM_PACK16:
case VK_FORMAT_R12X4_UNORM_PACK16:
case VK_FORMAT_A4R4G4B4_UNORM_PACK16:
case VK_FORMAT_A4B4G4R4_UNORM_PACK16:
case VK_FORMAT_R4G4B4A4_UNORM_PACK16:
case VK_FORMAT_B4G4R4A4_UNORM_PACK16:
case VK_FORMAT_R5G6B5_UNORM_PACK16:
case VK_FORMAT_B5G6R5_UNORM_PACK16:
case VK_FORMAT_R5G5B5A1_UNORM_PACK16:
case VK_FORMAT_B5G5R5A1_UNORM_PACK16:
case VK_FORMAT_A1R5G5B5_UNORM_PACK16:
case VK_FORMAT_R8G8_UNORM:
case VK_FORMAT_R8G8_SNORM:
case VK_FORMAT_R8G8_USCALED:
@@ -266,9 +268,14 @@ uint8_t getTexelBlockSize(VkFormat format) {
case VK_FORMAT_R8G8_UINT:
case VK_FORMAT_R8G8_SINT:
case VK_FORMAT_R8G8_SRGB:
case VK_FORMAT_R5G6B5_UNORM_PACK16:
case VK_FORMAT_R5G5B5A1_UNORM_PACK16:
case VK_FORMAT_R4G4B4A4_UNORM_PACK16:
case VK_FORMAT_R16_UNORM:
case VK_FORMAT_R16_SNORM:
case VK_FORMAT_R16_USCALED:
case VK_FORMAT_R16_SSCALED:
case VK_FORMAT_R16_UINT:
case VK_FORMAT_R16_SINT:
case VK_FORMAT_R16_SFLOAT:
case VK_FORMAT_D16_UNORM:
return 2;
// 24-bit formats.
@@ -286,19 +293,18 @@ uint8_t getTexelBlockSize(VkFormat format) {
case VK_FORMAT_B8G8R8_UINT:
case VK_FORMAT_B8G8R8_SINT:
case VK_FORMAT_B8G8R8_SRGB:
case VK_FORMAT_D16_UNORM_S8_UINT:
case VK_FORMAT_G8_B8_R8_3PLANE_420_UNORM:
case VK_FORMAT_G8_B8R8_2PLANE_420_UNORM:
case VK_FORMAT_G8_B8_R8_3PLANE_422_UNORM:
case VK_FORMAT_G8_B8R8_2PLANE_422_UNORM:
case VK_FORMAT_G8_B8_R8_3PLANE_444_UNORM:
case VK_FORMAT_G8_B8R8_2PLANE_444_UNORM:
return 3;
// 32-bit formats.
case VK_FORMAT_R32_UINT:
case VK_FORMAT_R32_SINT:
case VK_FORMAT_R32_SFLOAT:
case VK_FORMAT_R16G16_UNORM:
case VK_FORMAT_R16G16_SNORM:
case VK_FORMAT_R16G16_USCALED:
case VK_FORMAT_R16G16_SSCALED:
case VK_FORMAT_R16G16_UINT:
case VK_FORMAT_R16G16_SINT:
case VK_FORMAT_R16G16_SFLOAT:
case VK_FORMAT_R10X6G10X6_UNORM_2PACK16:
case VK_FORMAT_R12X4G12X4_UNORM_2PACK16:
case VK_FORMAT_R8G8B8A8_UNORM:
case VK_FORMAT_R8G8B8A8_SNORM:
case VK_FORMAT_R8G8B8A8_USCALED:
@@ -320,18 +326,41 @@ uint8_t getTexelBlockSize(VkFormat format) {
case VK_FORMAT_A8B8G8R8_UINT_PACK32:
case VK_FORMAT_A8B8G8R8_SINT_PACK32:
case VK_FORMAT_A8B8G8R8_SRGB_PACK32:
case VK_FORMAT_A2R10G10B10_UNORM_PACK32:
case VK_FORMAT_A2R10G10B10_SNORM_PACK32:
case VK_FORMAT_A2R10G10B10_USCALED_PACK32:
case VK_FORMAT_A2R10G10B10_SSCALED_PACK32:
case VK_FORMAT_A2R10G10B10_UINT_PACK32:
case VK_FORMAT_A2R10G10B10_SINT_PACK32:
case VK_FORMAT_A2B10G10R10_UNORM_PACK32:
// Depth and stencil formats.
case VK_FORMAT_S8_UINT:
case VK_FORMAT_D16_UNORM:
case VK_FORMAT_A2B10G10R10_SNORM_PACK32:
case VK_FORMAT_A2B10G10R10_USCALED_PACK32:
case VK_FORMAT_A2B10G10R10_SSCALED_PACK32:
case VK_FORMAT_A2B10G10R10_UINT_PACK32:
case VK_FORMAT_A2B10G10R10_SINT_PACK32:
case VK_FORMAT_R16G16_UNORM:
case VK_FORMAT_R16G16_SNORM:
case VK_FORMAT_R16G16_USCALED:
case VK_FORMAT_R16G16_SSCALED:
case VK_FORMAT_R16G16_UINT:
case VK_FORMAT_R16G16_SINT:
case VK_FORMAT_R16G16_SFLOAT:
case VK_FORMAT_R32_UINT:
case VK_FORMAT_R32_SINT:
case VK_FORMAT_R32_SFLOAT:
case VK_FORMAT_B10G11R11_UFLOAT_PACK32:
case VK_FORMAT_E5B9G9R9_UFLOAT_PACK32:
case VK_FORMAT_X8_D24_UNORM_PACK32:
case VK_FORMAT_D32_SFLOAT:
case VK_FORMAT_D24_UNORM_S8_UINT:
case VK_FORMAT_D32_SFLOAT_S8_UINT:
case VK_FORMAT_B10G11R11_UFLOAT_PACK32:
case VK_FORMAT_E5B9G9R9_UFLOAT_PACK32:
case VK_FORMAT_G8B8G8R8_422_UNORM:
case VK_FORMAT_B8G8R8G8_422_UNORM:
return 4;
// 40-bit formats.
case VK_FORMAT_D32_SFLOAT_S8_UINT:
return 5;
// 48-bit formats.
case VK_FORMAT_R16G16B16_UNORM:
case VK_FORMAT_R16G16B16_SNORM:
@@ -340,12 +369,27 @@ uint8_t getTexelBlockSize(VkFormat format) {
case VK_FORMAT_R16G16B16_UINT:
case VK_FORMAT_R16G16B16_SINT:
case VK_FORMAT_R16G16B16_SFLOAT:
case VK_FORMAT_G10X6_B10X6_R10X6_3PLANE_420_UNORM_3PACK16:
case VK_FORMAT_G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16:
case VK_FORMAT_G10X6_B10X6_R10X6_3PLANE_422_UNORM_3PACK16:
case VK_FORMAT_G10X6_B10X6R10X6_2PLANE_422_UNORM_3PACK16:
case VK_FORMAT_G10X6_B10X6_R10X6_3PLANE_444_UNORM_3PACK16:
case VK_FORMAT_G12X4_B12X4_R12X4_3PLANE_420_UNORM_3PACK16:
case VK_FORMAT_G12X4_B12X4R12X4_2PLANE_420_UNORM_3PACK16:
case VK_FORMAT_G12X4_B12X4_R12X4_3PLANE_422_UNORM_3PACK16:
case VK_FORMAT_G12X4_B12X4R12X4_2PLANE_422_UNORM_3PACK16:
case VK_FORMAT_G12X4_B12X4_R12X4_3PLANE_444_UNORM_3PACK16:
case VK_FORMAT_G16_B16_R16_3PLANE_420_UNORM:
case VK_FORMAT_G16_B16R16_2PLANE_420_UNORM:
case VK_FORMAT_G16_B16_R16_3PLANE_422_UNORM:
case VK_FORMAT_G16_B16R16_2PLANE_422_UNORM:
case VK_FORMAT_G16_B16_R16_3PLANE_444_UNORM:
case VK_FORMAT_G10X6_B10X6R10X6_2PLANE_444_UNORM_3PACK16:
case VK_FORMAT_G12X4_B12X4R12X4_2PLANE_444_UNORM_3PACK16:
case VK_FORMAT_G16_B16R16_2PLANE_444_UNORM:
return 6;
// 64-bit formats.
case VK_FORMAT_R32G32_UINT:
case VK_FORMAT_R32G32_SINT:
case VK_FORMAT_R32G32_SFLOAT:
case VK_FORMAT_R16G16B16A16_UNORM:
case VK_FORMAT_R16G16B16A16_SNORM:
case VK_FORMAT_R16G16B16A16_USCALED:
@@ -353,7 +397,12 @@ uint8_t getTexelBlockSize(VkFormat format) {
case VK_FORMAT_R16G16B16A16_UINT:
case VK_FORMAT_R16G16B16A16_SINT:
case VK_FORMAT_R16G16B16A16_SFLOAT:
// Compressed formats.
case VK_FORMAT_R32G32_UINT:
case VK_FORMAT_R32G32_SINT:
case VK_FORMAT_R32G32_SFLOAT:
case VK_FORMAT_R64_UINT:
case VK_FORMAT_R64_SINT:
case VK_FORMAT_R64_SFLOAT:
case VK_FORMAT_BC1_RGB_UNORM_BLOCK:
case VK_FORMAT_BC1_RGB_SRGB_BLOCK:
case VK_FORMAT_BC1_RGBA_UNORM_BLOCK:
@@ -366,6 +415,22 @@ uint8_t getTexelBlockSize(VkFormat format) {
case VK_FORMAT_ETC2_R8G8B8A1_SRGB_BLOCK:
case VK_FORMAT_EAC_R11_UNORM_BLOCK:
case VK_FORMAT_EAC_R11_SNORM_BLOCK:
case VK_FORMAT_R10X6G10X6B10X6A10X6_UNORM_4PACK16:
case VK_FORMAT_G10X6B10X6G10X6R10X6_422_UNORM_4PACK16:
case VK_FORMAT_B10X6G10X6R10X6G10X6_422_UNORM_4PACK16:
case VK_FORMAT_R12X4G12X4B12X4A12X4_UNORM_4PACK16:
case VK_FORMAT_G12X4B12X4G12X4R12X4_422_UNORM_4PACK16:
case VK_FORMAT_B12X4G12X4R12X4G12X4_422_UNORM_4PACK16:
case VK_FORMAT_G16B16G16R16_422_UNORM:
case VK_FORMAT_B16G16R16G16_422_UNORM:
case VK_FORMAT_PVRTC1_2BPP_UNORM_BLOCK_IMG:
case VK_FORMAT_PVRTC1_2BPP_SRGB_BLOCK_IMG:
case VK_FORMAT_PVRTC1_4BPP_UNORM_BLOCK_IMG:
case VK_FORMAT_PVRTC1_4BPP_SRGB_BLOCK_IMG:
case VK_FORMAT_PVRTC2_2BPP_UNORM_BLOCK_IMG:
case VK_FORMAT_PVRTC2_2BPP_SRGB_BLOCK_IMG:
case VK_FORMAT_PVRTC2_4BPP_UNORM_BLOCK_IMG:
case VK_FORMAT_PVRTC2_4BPP_SRGB_BLOCK_IMG:
return 8;
// 96-bit formats.
@@ -375,54 +440,82 @@ uint8_t getTexelBlockSize(VkFormat format) {
return 12;
// 128-bit formats.
case VK_FORMAT_R32G32B32A32_UINT:
case VK_FORMAT_R32G32B32A32_SINT:
case VK_FORMAT_R32G32B32A32_SFLOAT:
// Compressed formats.
case VK_FORMAT_BC2_UNORM_BLOCK:
case VK_FORMAT_BC2_SRGB_BLOCK:
case VK_FORMAT_BC3_UNORM_BLOCK:
case VK_FORMAT_BC3_SRGB_BLOCK:
case VK_FORMAT_BC5_UNORM_BLOCK:
case VK_FORMAT_BC5_SNORM_BLOCK:
case VK_FORMAT_BC6H_SFLOAT_BLOCK:
case VK_FORMAT_BC6H_UFLOAT_BLOCK:
case VK_FORMAT_BC6H_SFLOAT_BLOCK:
case VK_FORMAT_BC7_UNORM_BLOCK:
case VK_FORMAT_BC7_SRGB_BLOCK:
case VK_FORMAT_ASTC_4x4_UNORM_BLOCK:
case VK_FORMAT_ASTC_5x4_UNORM_BLOCK:
case VK_FORMAT_ASTC_5x5_UNORM_BLOCK:
case VK_FORMAT_ASTC_6x5_UNORM_BLOCK:
case VK_FORMAT_ASTC_6x6_UNORM_BLOCK:
case VK_FORMAT_ASTC_8x5_UNORM_BLOCK:
case VK_FORMAT_ASTC_8x6_UNORM_BLOCK:
case VK_FORMAT_ASTC_8x8_UNORM_BLOCK:
case VK_FORMAT_ASTC_10x5_UNORM_BLOCK:
case VK_FORMAT_ASTC_10x6_UNORM_BLOCK:
case VK_FORMAT_ASTC_10x8_UNORM_BLOCK:
case VK_FORMAT_ASTC_10x10_UNORM_BLOCK:
case VK_FORMAT_ASTC_12x10_UNORM_BLOCK:
case VK_FORMAT_ASTC_12x12_UNORM_BLOCK:
case VK_FORMAT_ASTC_4x4_SRGB_BLOCK:
case VK_FORMAT_ASTC_5x4_SRGB_BLOCK:
case VK_FORMAT_ASTC_5x5_SRGB_BLOCK:
case VK_FORMAT_ASTC_6x5_SRGB_BLOCK:
case VK_FORMAT_ASTC_6x6_SRGB_BLOCK:
case VK_FORMAT_ASTC_8x5_SRGB_BLOCK:
case VK_FORMAT_ASTC_8x6_SRGB_BLOCK:
case VK_FORMAT_ASTC_8x8_SRGB_BLOCK:
case VK_FORMAT_ASTC_10x5_SRGB_BLOCK:
case VK_FORMAT_ASTC_10x6_SRGB_BLOCK:
case VK_FORMAT_ASTC_10x8_SRGB_BLOCK:
case VK_FORMAT_ASTC_10x10_SRGB_BLOCK:
case VK_FORMAT_ASTC_12x10_SRGB_BLOCK:
case VK_FORMAT_ASTC_12x12_SRGB_BLOCK:
case VK_FORMAT_ETC2_R8G8B8A8_UNORM_BLOCK:
case VK_FORMAT_ETC2_R8G8B8A8_SRGB_BLOCK:
case VK_FORMAT_EAC_R11G11_UNORM_BLOCK:
case VK_FORMAT_EAC_R11G11_SNORM_BLOCK:
case VK_FORMAT_ASTC_4x4_SFLOAT_BLOCK:
case VK_FORMAT_ASTC_4x4_UNORM_BLOCK:
case VK_FORMAT_ASTC_4x4_SRGB_BLOCK:
case VK_FORMAT_ASTC_5x4_SFLOAT_BLOCK:
case VK_FORMAT_ASTC_5x4_UNORM_BLOCK:
case VK_FORMAT_ASTC_5x4_SRGB_BLOCK:
case VK_FORMAT_ASTC_5x5_SFLOAT_BLOCK:
case VK_FORMAT_ASTC_5x5_UNORM_BLOCK:
case VK_FORMAT_ASTC_5x5_SRGB_BLOCK:
case VK_FORMAT_ASTC_6x5_SFLOAT_BLOCK:
case VK_FORMAT_ASTC_6x5_UNORM_BLOCK:
case VK_FORMAT_ASTC_6x5_SRGB_BLOCK:
case VK_FORMAT_ASTC_6x6_SFLOAT_BLOCK:
case VK_FORMAT_ASTC_6x6_UNORM_BLOCK:
case VK_FORMAT_ASTC_6x6_SRGB_BLOCK:
case VK_FORMAT_ASTC_8x5_SFLOAT_BLOCK:
case VK_FORMAT_ASTC_8x5_UNORM_BLOCK:
case VK_FORMAT_ASTC_8x5_SRGB_BLOCK:
case VK_FORMAT_ASTC_8x6_SFLOAT_BLOCK:
case VK_FORMAT_ASTC_8x6_UNORM_BLOCK:
case VK_FORMAT_ASTC_8x6_SRGB_BLOCK:
case VK_FORMAT_ASTC_8x8_SFLOAT_BLOCK:
case VK_FORMAT_ASTC_8x8_UNORM_BLOCK:
case VK_FORMAT_ASTC_8x8_SRGB_BLOCK:
case VK_FORMAT_ASTC_10x5_SFLOAT_BLOCK:
case VK_FORMAT_ASTC_10x5_UNORM_BLOCK:
case VK_FORMAT_ASTC_10x5_SRGB_BLOCK:
case VK_FORMAT_ASTC_10x6_SFLOAT_BLOCK:
case VK_FORMAT_ASTC_10x6_UNORM_BLOCK:
case VK_FORMAT_ASTC_10x6_SRGB_BLOCK:
case VK_FORMAT_ASTC_10x8_SFLOAT_BLOCK:
case VK_FORMAT_ASTC_10x8_UNORM_BLOCK:
case VK_FORMAT_ASTC_10x8_SRGB_BLOCK:
case VK_FORMAT_ASTC_10x10_SFLOAT_BLOCK:
case VK_FORMAT_ASTC_10x10_UNORM_BLOCK:
case VK_FORMAT_ASTC_10x10_SRGB_BLOCK:
case VK_FORMAT_ASTC_12x10_SFLOAT_BLOCK:
case VK_FORMAT_ASTC_12x10_UNORM_BLOCK:
case VK_FORMAT_ASTC_12x10_SRGB_BLOCK:
case VK_FORMAT_ASTC_12x12_SFLOAT_BLOCK:
case VK_FORMAT_ASTC_12x12_UNORM_BLOCK:
case VK_FORMAT_ASTC_12x12_SRGB_BLOCK:
case VK_FORMAT_R32G32B32A32_UINT:
case VK_FORMAT_R32G32B32A32_SINT:
case VK_FORMAT_R32G32B32A32_SFLOAT:
case VK_FORMAT_R64G64_UINT:
case VK_FORMAT_R64G64_SINT:
case VK_FORMAT_R64G64_SFLOAT:
return 16;
// 192-bit formats.
case VK_FORMAT_R64G64B64_UINT:
case VK_FORMAT_R64G64B64_SINT:
case VK_FORMAT_R64G64B64_SFLOAT:
return 24;
// 256-bit formats.
case VK_FORMAT_R64G64B64A64_UINT:
case VK_FORMAT_R64G64B64A64_SINT:
case VK_FORMAT_R64G64B64A64_SFLOAT:
return 32;
case VK_FORMAT_UNDEFINED:
// In cases where we've explicitly already determined that the
// format is not supported, let the rest of the system handle
@@ -438,7 +531,6 @@ uint8_t getTexelBlockSize(VkFormat format) {
}
}
VkFormat getVkFormat(PixelDataFormat format, PixelDataType type) {
if (type == PixelDataType::USHORT_565) return VK_FORMAT_R5G6B5_UNORM_PACK16;
if (type == PixelDataType::UINT_2_10_10_10_REV) return VK_FORMAT_A2B10G10R10_UNORM_PACK32;
@@ -37,8 +37,8 @@ namespace {
uint32_t size, const char* const label) {
// Write size must be divisible by WEBGPU_BUFFER_SIZE_MODULUS (e.g. 4).
// If the whole buffer is written to as is common, so must the buffer size.
size += (WEBGPU_BUFFER_SIZE_MODULUS - (size % WEBGPU_BUFFER_SIZE_MODULUS)) %
WEBGPU_BUFFER_SIZE_MODULUS;
size += (FILAMENT_WEBGPU_BUFFER_SIZE_MODULUS - (size % FILAMENT_WEBGPU_BUFFER_SIZE_MODULUS)) %
FILAMENT_WEBGPU_BUFFER_SIZE_MODULUS;
wgpu::BufferDescriptor descriptor{
.label = label,
.usage = usage,
@@ -62,15 +62,15 @@ void WebGPUBufferBase::updateGPUBuffer(BufferDescriptor const& bufferDescriptor,
FILAMENT_CHECK_PRECONDITION(bufferDescriptor.size + byteOffset <= mBuffer.GetSize())
<< "Attempting to copy " << bufferDescriptor.size << " bytes into a buffer of size "
<< mBuffer.GetSize() << " at offset " << byteOffset;
FILAMENT_CHECK_PRECONDITION(byteOffset % WEBGPU_BUFFER_SIZE_MODULUS == 0)
<< "Byte offset must be a multiple of " << WEBGPU_BUFFER_SIZE_MODULUS << " but is "
<< byteOffset;
FILAMENT_CHECK_PRECONDITION(byteOffset % FILAMENT_WEBGPU_BUFFER_SIZE_MODULUS == 0)
<< "Byte offset must be a multiple of " << FILAMENT_WEBGPU_BUFFER_SIZE_MODULUS
<< " but is " << byteOffset;
// TODO: All buffer objects are created with CopyDst usage.
// This may have some performance implications. That should be investigated later.
assert_invariant(mBuffer.GetUsage() & wgpu::BufferUsage::CopyDst);
const size_t remainder = bufferDescriptor.size % WEBGPU_BUFFER_SIZE_MODULUS;
const size_t remainder = bufferDescriptor.size % FILAMENT_WEBGPU_BUFFER_SIZE_MODULUS;
// WriteBuffer is an async call. But cpu buffer data is already written to the staging
// buffer on return from the WriteBuffer.
@@ -82,10 +82,11 @@ void WebGPUBufferBase::updateGPUBuffer(BufferDescriptor const& bufferDescriptor,
memcpy(mRemainderChunk.data(), remainderStart, remainder);
// Pad the remainder with zeros to ensure deterministic behavior, though GPU shouldn't
// access this
std::memset(mRemainderChunk.data() + remainder, 0, WEBGPU_BUFFER_SIZE_MODULUS - remainder);
std::memset(mRemainderChunk.data() + remainder, 0,
FILAMENT_WEBGPU_BUFFER_SIZE_MODULUS - remainder);
queue.WriteBuffer(mBuffer, byteOffset + legalSize, &mRemainderChunk,
WEBGPU_BUFFER_SIZE_MODULUS);
FILAMENT_WEBGPU_BUFFER_SIZE_MODULUS);
}
}
@@ -39,8 +39,8 @@ protected:
private:
const wgpu::Buffer mBuffer;
// WEBGPU_BUFFER_SIZE_MODULUS (e.g. 4) bytes to hold any extra chunk we need.
std::array<uint8_t, WEBGPU_BUFFER_SIZE_MODULUS> mRemainderChunk{};
// FILAMENT_WEBGPU_BUFFER_SIZE_MODULUS (e.g. 4) bytes to hold any extra chunk we need.
std::array<uint8_t, FILAMENT_WEBGPU_BUFFER_SIZE_MODULUS> mRemainderChunk{};
};
} // namespace filament::backend
+16 -4
View File
@@ -21,7 +21,7 @@
#include <cstdint>
constexpr size_t WEBGPU_BUFFER_SIZE_MODULUS = 4;
constexpr size_t FILAMENT_WEBGPU_BUFFER_SIZE_MODULUS = 4;
// FWGPU is short for Filament WebGPU
@@ -65,13 +65,25 @@ constexpr size_t WEBGPU_BUFFER_SIZE_MODULUS = 4;
#define FWGPU_LOGI LOG(INFO)
#endif
constexpr uint64_t REQUEST_ADAPTER_TIMEOUT_NANOSECONDS =
constexpr uint64_t FILAMENT_WEBGPU_REQUEST_ADAPTER_TIMEOUT_NANOSECONDS =
/* milliseconds */ 1000u * /* converted to ns */ 1000000u;
constexpr uint64_t REQUEST_DEVICE_TIMEOUT_NANOSECONDS =
constexpr uint64_t FILAMENT_WEBGPU_REQUEST_DEVICE_TIMEOUT_NANOSECONDS =
/* milliseconds */ 1000u * /* converted to ns */ 1000000u;
constexpr uint64_t SHADER_COMPILATION_TIMEOUT_NANOSECONDS =
constexpr uint64_t FILAMENT_WEBGPU_SHADER_COMPILATION_TIMEOUT_NANOSECONDS =
/* milliseconds */ 1000u * /* converted to ns */ 1000000u;
// if a render pipeline is not used in this number of consecutive frames,
// then expire/release it from the cache.
// A smaller number means more frequent pipeline creation events, taking more time.
// A larger number means more pipelines stored, taking more memory.
constexpr uint64_t FILAMENT_WEBGPU_RENDER_PIPELINE_EXPIRATION_IN_FRAME_COUNT = 45;
// if a pipeline layout is not used in this number of consecutive frames,
// then expire/release it from the cache.
// A smaller number means more frequent pipeline layout creation events, taking more time.
// A larger number means more layouts stored, taking more memory.
constexpr uint64_t FILAMENT_WEBGPU_PIPELINE_LAYOUT_EXPIRATION_IN_FRAME_COUNT = 90;
#endif// TNT_FILAMENT_BACKEND_WEBGPUCONSTANTS_H
+135 -197
View File
@@ -20,7 +20,8 @@
#include "WebGPUDescriptorSetLayout.h"
#include "WebGPUFence.h"
#include "WebGPUIndexBuffer.h"
#include "WebGPUPipelineCreation.h"
#include "WebGPUPipelineCache.h"
#include "WebGPUPipelineLayoutCache.h"
#include "WebGPUProgram.h"
#include "WebGPURenderPrimitive.h"
#include "WebGPURenderTarget.h"
@@ -72,6 +73,8 @@ WebGPUDriver::WebGPUDriver(WebGPUPlatform& platform,
mAdapter{ mPlatform.requestAdapter(nullptr) },
mDevice{ mPlatform.requestDevice(mAdapter) },
mQueue{ mDevice.GetQueue() },
mPipelineLayoutCache{ mDevice },
mPipelineCache{ mDevice },
mRenderPassMipmapGenerator{ mDevice },
mSpdComputePassMipmapGenerator{ mDevice },
mHandleAllocator{ "Handles", driverConfig.handleArenaSize,
@@ -126,7 +129,9 @@ void WebGPUDriver::setFrameCompletedCallback(Handle<HwSwapChain> sch,
void WebGPUDriver::setPresentationTime(int64_t monotonic_clock_ns) {
}
void WebGPUDriver::endFrame(uint32_t frameId) {
void WebGPUDriver::endFrame(const uint32_t /* frameId */) {
mPipelineLayoutCache.onFrameEnd();
mPipelineCache.onFrameEnd();
}
void WebGPUDriver::flush(int) {
@@ -457,8 +462,13 @@ void WebGPUDriver::createRenderTargetR(Handle<HwRenderTarget> renderTargetHandle
const TargetBufferFlags targetFlags, const uint32_t width, const uint32_t height,
const uint8_t samples, const uint8_t layerCount, const MRT color,
const TargetBufferInfo depth, const TargetBufferInfo stencil) {
constructHandle<WebGPURenderTarget>(renderTargetHandle, width, height, samples, layerCount,
color, depth, stencil, targetFlags);
constructHandle<WebGPURenderTarget>(
renderTargetHandle, width, height, samples, layerCount, color, depth, stencil,
targetFlags,
[&](const Handle<HwTexture> textureHandle) {
return handleCast<WebGPUTexture>(textureHandle);
},
mDevice);
}
void WebGPUDriver::createFenceR(Handle<HwFence> fenceHandle, const int /* dummy */) {
@@ -586,7 +596,7 @@ bool WebGPUDriver::isParallelShaderCompileSupported() {
}
bool WebGPUDriver::isDepthStencilResolveSupported() {
return true;
return false;
}
bool WebGPUDriver::isDepthStencilBlitSupported(const TextureFormat format) {
@@ -819,10 +829,15 @@ void WebGPUDriver::beginRenderPass(Handle<HwRenderTarget> renderTargetHandle,
wgpu::TextureView defaultDepthStencilView = nullptr;
wgpu::TextureFormat defaultDepthStencilFormat = wgpu::TextureFormat::Undefined;
const bool msaaSidecarsRequired{ renderTarget->getSamples() > 1 &&
renderTarget->getSampleCountPerAttachment() <= 1 };
std::array<wgpu::TextureView, MRT::MAX_SUPPORTED_RENDER_TARGET_COUNT> customColorViews{};
std::array<wgpu::TextureView, customColorViews.size()> customColorMsaaSidecarViews{};
uint32_t customColorViewCount = 0;
wgpu::TextureView customDepthStencilView = nullptr;
wgpu::TextureView customDepthStencilMsaaSidecarTextureView = nullptr;
wgpu::TextureFormat customDepthStencilFormat = wgpu::TextureFormat::Undefined;
mCurrentRenderTarget = renderTarget;
@@ -833,21 +848,17 @@ void WebGPUDriver::beginRenderPass(Handle<HwRenderTarget> renderTargetHandle,
defaultDepthStencilFormat = mSwapChain->getDepthFormat();
if (any(renderTarget->getTargetFlags() & TargetBufferFlags::STENCIL) &&
!(defaultDepthStencilFormat == wgpu::TextureFormat::Depth24PlusStencil8 ||
defaultDepthStencilFormat == wgpu::TextureFormat::Depth32FloatStencil8 ||
defaultDepthStencilFormat == wgpu::TextureFormat::Stencil8)) {
FILAMENT_CHECK_POSTCONDITION(false) << "Default render target requested stencil, but swap chain's depth format "
<< (uint32_t)defaultDepthStencilFormat << " does not have a stencil aspect.";
!(hasStencil(defaultDepthStencilFormat))) {
FILAMENT_CHECK_POSTCONDITION(false)
<< "Default render target requested stencil, but swap chain's depth format "
<< (uint32_t) defaultDepthStencilFormat << " does not have a stencil aspect.";
}
if (any(renderTarget->getTargetFlags() & TargetBufferFlags::DEPTH) &&
!(defaultDepthStencilFormat == wgpu::TextureFormat::Depth16Unorm ||
defaultDepthStencilFormat == wgpu::TextureFormat::Depth32Float ||
defaultDepthStencilFormat == wgpu::TextureFormat::Depth24Plus ||
defaultDepthStencilFormat == wgpu::TextureFormat::Depth24PlusStencil8 ||
defaultDepthStencilFormat == wgpu::TextureFormat::Depth32FloatStencil8)) {
FILAMENT_CHECK_POSTCONDITION(false) << "Default render target requested depth, but swap chain's depth format "
<< (uint32_t)defaultDepthStencilFormat << " does not have a depth aspect.";
!(hasDepth(defaultDepthStencilFormat))) {
FILAMENT_CHECK_POSTCONDITION(false)
<< "Default render target requested depth, but swap chain's depth format "
<< (uint32_t) defaultDepthStencilFormat << " does not have a depth aspect.";
}
} else {
// Resolve views for custom render target
@@ -863,8 +874,19 @@ void WebGPUDriver::beginRenderPass(Handle<HwRenderTarget> renderTargetHandle,
<< ".";
const uint8_t mipLevel = colorInfos[i].level;
const uint32_t arrayLayer = colorInfos[i].layer;
customColorViews[customColorViewCount++] =
customColorViews[customColorViewCount] =
colorTexture->getOrMakeTextureView(mipLevel, arrayLayer);
if (msaaSidecarsRequired) {
const wgpu::TextureView msaaSidecarView{
colorTexture->makeMsaaSidecarTextureViewIfTextureSidecarExists(
renderTarget->getSamples(), mipLevel, arrayLayer)
};
FILAMENT_CHECK_POSTCONDITION(msaaSidecarView)
<< "Could not get a required MSAA sidecar texture view for color "
<< customColorViewCount << "?";
customColorMsaaSidecarViews[customColorViewCount] = msaaSidecarView;
}
customColorViewCount++;
}
}
}
@@ -896,15 +918,21 @@ void WebGPUDriver::beginRenderPass(Handle<HwRenderTarget> renderTargetHandle,
if (depthStencilSourceHandle) {
auto dsTexture = handleCast<WebGPUTexture>(depthStencilSourceHandle);
if (dsTexture) {
customDepthStencilView = dsTexture->getOrMakeTextureView(depthStencilMipLevel, depthStencilArrayLayer);
customDepthStencilView = dsTexture->getOrMakeTextureView(depthStencilMipLevel,
depthStencilArrayLayer);
if (msaaSidecarsRequired) {
customDepthStencilMsaaSidecarTextureView =
dsTexture->makeMsaaSidecarTextureViewIfTextureSidecarExists(
renderTarget->getSamples(), depthStencilMipLevel,
depthStencilArrayLayer);
FILAMENT_CHECK_POSTCONDITION(customDepthStencilMsaaSidecarTextureView)
<< "Could not get a required MSAA sidecar texture view for "
"depth/stencil?";
}
customDepthStencilFormat = dsTexture->getViewFormat();
if (any(renderTarget->getTargetFlags() & TargetBufferFlags::STENCIL) &&
!(customDepthStencilFormat == wgpu::TextureFormat::Depth24PlusStencil8 ||
customDepthStencilFormat ==
wgpu::TextureFormat::Depth32FloatStencil8 ||
customDepthStencilFormat ==
wgpu::TextureFormat::Stencil8)) {
!(hasStencil(customDepthStencilFormat))) {
FILAMENT_CHECK_POSTCONDITION(false)
<< "Custom render target requested stencil, but the provided texture"
"format number"
@@ -912,14 +940,7 @@ void WebGPUDriver::beginRenderPass(Handle<HwRenderTarget> renderTargetHandle,
<< " does not have a stencil aspect.";
}
if (any(renderTarget->getTargetFlags() & TargetBufferFlags::DEPTH) &&
!(customDepthStencilFormat == wgpu::TextureFormat::Depth16Unorm ||
customDepthStencilFormat == wgpu::TextureFormat::Depth32Float ||
customDepthStencilFormat ==
wgpu::TextureFormat::Depth24Plus ||
customDepthStencilFormat ==
wgpu::TextureFormat::Depth24PlusStencil8 ||
customDepthStencilFormat ==
wgpu::TextureFormat::Depth32FloatStencil8)) {
!(hasDepth(customDepthStencilFormat))) {
FILAMENT_CHECK_POSTCONDITION(false) << "Custom render target requested depth, "
"but the provided texture format number"
<< (uint32_t) customDepthStencilFormat
@@ -934,8 +955,10 @@ void WebGPUDriver::beginRenderPass(Handle<HwRenderTarget> renderTargetHandle,
defaultColorView,
defaultDepthStencilView,
customColorViews.data(),
customColorMsaaSidecarViews.data(),
customColorViewCount,
customDepthStencilView);
customDepthStencilView,
customDepthStencilMsaaSidecarTextureView);
mRenderPassEncoder = mCommandEncoder.BeginRenderPass(&renderPassDescriptor);
@@ -976,13 +999,13 @@ void WebGPUDriver::makeCurrent(Handle<HwSwapChain> drawSch, Handle<HwSwapChain>
wgpu::TextureFormat depthFormat = mSwapChain->getDepthFormat();
TargetBufferFlags newTargetFlags = filament::backend::TargetBufferFlags::NONE;
//Assuming Color and Depth are always present.
//Assuming Color always present in default render target.
newTargetFlags |= filament::backend::TargetBufferFlags::COLOR;
if (depthFormat != wgpu::TextureFormat::Undefined) {
newTargetFlags |= filament::backend::TargetBufferFlags::DEPTH;
if (depthFormat == wgpu::TextureFormat::Depth24PlusStencil8 ||
depthFormat == wgpu::TextureFormat::Depth32FloatStencil8) {
if (hasDepth(depthFormat)) {
newTargetFlags |= filament::backend::TargetBufferFlags::DEPTH;
}
if (hasStencil(depthFormat)) {
newTargetFlags |= filament::backend::TargetBufferFlags::STENCIL;
}
}
@@ -1069,102 +1092,33 @@ 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.";
FILAMENT_CHECK_PRECONDITION(mRenderPassEncoder == nullptr)
<< "Resolve cannot be called during an existing render pass";
const auto sourceTexture{ handleCast<WebGPUTexture>(sourceTextureHandle) };
const auto destinationTexture{ handleCast<WebGPUTexture>(destinationTextureHandle) };
assert_invariant(sourceTexture);
assert_invariant(destinationTexture);
FILAMENT_CHECK_PRECONDITION(destinationTexture->width == sourceTexture->width &&
destinationTexture->height == sourceTexture->height)
<< "invalid resolve: source and destination sizes don't match";
FILAMENT_CHECK_PRECONDITION(sourceTexture->samples > 1 && destinationTexture->samples == 1)
<< "invalid resolve: source.samples=" << +sourceTexture->samples
<< ", destination.samples=" << +destinationTexture->samples;
FILAMENT_CHECK_PRECONDITION(sourceTexture->format == destinationTexture->format)
<< "source and destination texture format don't match";
FILAMENT_CHECK_PRECONDITION(!isDepthFormat(sourceTexture->format))
<< "can't resolve depth formats";
FILAMENT_CHECK_PRECONDITION(!isStencilFormat(sourceTexture->format))
<< "can't resolve stencil formats";
FILAMENT_CHECK_PRECONDITION(any(destinationTexture->usage & TextureUsage::BLIT_DST))
<< "destination texture doesn't have BLIT_DST";
FILAMENT_CHECK_PRECONDITION(any(sourceTexture->usage & TextureUsage::BLIT_SRC))
<< "source texture doesn't have BLIT_SRC";
FILAMENT_CHECK_PRECONDITION(sourceTexture->getTexture().GetUsage() & wgpu::TextureUsage::RenderAttachment)
<< "source texture usage doesn't have wgpu::TextureUsage::RenderAttachment";
FILAMENT_CHECK_PRECONDITION(destinationTexture->getTexture().GetUsage() & wgpu::TextureUsage::RenderAttachment)
<< "destination texture usage doesn't have wgpu::TextureUsage::RenderAttachment";
FILAMENT_CHECK_PRECONDITION(destinationTexture->getTexture().GetUsage() & wgpu::TextureUsage::TextureBinding)
<< "destination texture usage doesn't have wgpu::TextureUsage::TextureBinding";
const wgpu::TextureFormat format{ sourceTexture->getViewFormat() };
const wgpu::TextureViewDescriptor sourceTextureViewDescriptor{
.label = "resolve_source_texture_view",
.format = format,
.dimension = sourceTexture->getViewDimension(),
.baseMipLevel = sourceLevel,
.mipLevelCount = 1,
.baseArrayLayer = sourceLayer,
.arrayLayerCount = 1,
.aspect = sourceTexture->getAspect(),
.usage = sourceTexture->getTexture().GetUsage(),
const WebGPUMsaaTextureResolver::ResolveRequest request{
.commandEncoder = mCommandEncoder,
.viewFormat = sourceTexture->getViewFormat(),
.source = {
.texture = sourceTexture->getTexture(),
.viewDimension = sourceTexture->getViewDimension(),
.mipLevel = sourceLevel,
.layer = sourceLayer,
.aspect = sourceTexture->getAspect(),
},
.destination = {
.texture = destinationTexture->getTexture(),
.viewDimension = destinationTexture->getViewDimension(),
.mipLevel = destinationLevel,
.layer = destinationLayer,
.aspect = destinationTexture->getAspect(),
},
};
const wgpu::TextureView sourceTextureView{ sourceTexture->getTexture().CreateView(
&sourceTextureViewDescriptor) };
FILAMENT_CHECK_POSTCONDITION(sourceTextureView) << "Failed to create wgpu::TextureView sourceTextureView";
const wgpu::TextureViewDescriptor destinationTextureViewDescriptor{
.label = "resolve_destination_texture_view",
.format = format,
.dimension = destinationTexture->getViewDimension(),
.baseMipLevel = destinationLevel,
.mipLevelCount = 1,
.baseArrayLayer = destinationLayer,
.arrayLayerCount = 1,
.aspect = destinationTexture->getAspect(),
.usage = destinationTexture->getTexture().GetUsage(),
};
const wgpu::TextureView destinationTextureView{ destinationTexture->getTexture().CreateView(
&destinationTextureViewDescriptor) };
FILAMENT_CHECK_POSTCONDITION(destinationTextureView) << "Failed to create wgpu::TextureView destinationTextureView.";
const wgpu::RenderPassColorAttachment colorAttachment{
.view = sourceTextureView,
.depthSlice = wgpu::kDepthSliceUndefined, // being explicit for consistent behavior
.resolveTarget = destinationTextureView,
.loadOp = wgpu::LoadOp::Load,
.storeOp = wgpu::StoreOp::Store,
.clearValue = {}, // being explicit for consistent behavior
};
const wgpu::RenderPassDescriptor renderPassDescriptor{
.label = "resolve_render_pass",
.colorAttachmentCount = 1,
.colorAttachments = &colorAttachment,
.depthStencilAttachment = nullptr, // being explicit for consistent behavior
.occlusionQuerySet = nullptr, // being explicit for consistent behavior
.timestampWrites = nullptr, // being explicit for consistent behavior
};
const wgpu::RenderPassEncoder renderPassEncoder{ mCommandEncoder.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
mMsaaTextureResolver.resolve(request);
}
void WebGPUDriver::blit(Handle<HwTexture> destinationTextureHandle, const uint8_t sourceLevel,
@@ -1174,110 +1128,94 @@ void WebGPUDriver::blit(Handle<HwTexture> destinationTextureHandle, const uint8_
// todo
}
size_t WebGPUDriver::computePipelineKey(PipelineState const& pipelineState,
WebGPURenderTarget const* const renderTarget) const {
// TODO Investigate implications of this hash more closely. Vulkan has a whole class
// VulkanPipelineCache to handle this, may be missing nuance
static const auto pipelineStateHasher{
utils::hash::MurmurHashFn<filament::backend::PipelineState>()
};
const std::hash<uint32_t> intHasher{};
const std::hash<WebGPURenderTarget const*> addressHasher{};
const size_t pipelineStateHash{ intHasher(pipelineStateHasher(pipelineState)) };
const size_t renderTargetHash{ addressHasher(renderTarget) };
return utils::hash::combine(pipelineStateHash, renderTargetHash);
}
void WebGPUDriver::bindPipeline(PipelineState const& pipelineState) {
auto pipelineKey{ computePipelineKey(pipelineState, mCurrentRenderTarget) };
if (mPipelineMap.find(pipelineKey) != mPipelineMap.end()) {
mRenderPassEncoder.SetPipeline(mPipelineMap[pipelineKey]);
return;
}
const auto program = handleCast<WebGPUProgram>(pipelineState.program);
assert_invariant(mRenderPassEncoder);
const auto program{ handleCast<WebGPUProgram>(pipelineState.program) };
assert_invariant(program);
WebGPURenderTarget const* renderTarget{ mCurrentRenderTarget };
assert_invariant(renderTarget);
assert_invariant(program->computeShaderModule == nullptr &&
"WebGPU backend does not (yet) support compute pipelines.");
FILAMENT_CHECK_POSTCONDITION(program->vertexShaderModule)
<< "WebGPU backend requires a vertex shader module for a render pipeline";
const auto vertexBufferInfo{ handleCast<WebGPUVertexBufferInfo>(
pipelineState.vertexBufferInfo) };
assert_invariant(vertexBufferInfo);
std::array<wgpu::BindGroupLayout, MAX_DESCRIPTOR_SET_COUNT> bindGroupLayouts{};
assert_invariant(bindGroupLayouts.size() >= pipelineState.pipelineLayout.setLayout.size());
size_t bindGroupLayoutCount = 0;
for (size_t i = 0; i < bindGroupLayouts.size(); i++) {
const auto handle = pipelineState.pipelineLayout.setLayout[bindGroupLayoutCount];
size_t bindGroupLayoutCount{ 0 };
for (size_t i{ 0 }; i < bindGroupLayouts.size(); i++) {
const auto handle{ pipelineState.pipelineLayout.setLayout[bindGroupLayoutCount] };
if (handle.getId() == HandleBase::nullid) {
continue;
}
bindGroupLayouts[bindGroupLayoutCount++] =
handleCast<WebGPUDescriptorSetLayout>(handle)->getLayout();
}
std::stringstream layoutLabelStream;
layoutLabelStream << program->name.c_str() << " layout";
const auto layoutLabel = layoutLabelStream.str();
const wgpu::PipelineLayoutDescriptor layoutDescriptor{
.label = wgpu::StringView(layoutLabel),
const WebGPUPipelineLayoutCache::PipelineLayoutRequest pipelineLayoutRequest{
.label = program->name,
.bindGroupLayouts = bindGroupLayouts,
.bindGroupLayoutCount = bindGroupLayoutCount,
.bindGroupLayouts = bindGroupLayouts.data()
// TODO investigate immediateDataRangeByteSize
};
const wgpu::PipelineLayout layout = mDevice.CreatePipelineLayout(&layoutDescriptor);
FILAMENT_CHECK_POSTCONDITION(layout)
<< "Failed to create wgpu::PipelineLayout for render pipeline for "
<< layoutDescriptor.label;
const auto vertexBufferInfo =
handleCast<WebGPUVertexBufferInfo>(pipelineState.vertexBufferInfo);
assert_invariant(vertexBufferInfo);
std::vector<wgpu::TextureFormat> pipelineColorFormats;
wgpu::TextureFormat pipelineDepthStencilFormat = wgpu::TextureFormat::Undefined;
uint8_t pipelineSamples = 1;
bool const requestedDepth = any(mCurrentRenderTarget->getTargetFlags() & TargetBufferFlags::DEPTH);
bool const requestedStencil = any(mCurrentRenderTarget->getTargetFlags() & TargetBufferFlags::STENCIL);
pipelineSamples = mCurrentRenderTarget->getSamples();
if (mCurrentRenderTarget->isDefaultRenderTarget()) {
pipelineColorFormats.push_back(mSwapChain->getColorFormat());
pipelineDepthStencilFormat = mSwapChain->getDepthFormat();
wgpu::PipelineLayout const& layout{ mPipelineLayoutCache.getOrCreatePipelineLayout(
pipelineLayoutRequest) };
uint8_t colorFormatCount{ 0 };
std::array<wgpu::TextureFormat, MRT::MAX_SUPPORTED_RENDER_TARGET_COUNT> colorFormats{
wgpu::TextureFormat::Undefined
};
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 {
const auto& mrtColorAttachments = mCurrentRenderTarget->getColorAttachmentInfos();
for (size_t i = 0; i < MRT::MAX_SUPPORTED_RENDER_TARGET_COUNT; ++i) {
// 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) {
const auto colorTexture = handleCast<WebGPUTexture>(mrtColorAttachments[i].handle);
const auto colorTexture{ handleCast<WebGPUTexture>(mrtColorAttachments[i].handle) };
if (colorTexture) {
pipelineColorFormats.push_back(colorTexture->getTexture().GetFormat());
colorFormats[colorFormatCount++] = colorTexture->getTexture().GetFormat();
}
}
}
// custom render target depth/stencil...
const auto& depthInfo = mCurrentRenderTarget->getDepthAttachmentInfo();
const auto& stencilInfo = mCurrentRenderTarget->getStencilAttachmentInfo();
Handle<HwTexture> depthStencilHandle = {};
Handle<HwTexture> depthStencilHandle{};
if (depthInfo.handle) {
depthStencilHandle = depthInfo.handle;
} else if (stencilInfo.handle) {
depthStencilHandle = stencilInfo.handle;
depthStencilHandle = stencilInfo.handle;
}
if (depthStencilHandle) {
const auto dsTexture = handleCast<WebGPUTexture>(depthStencilHandle);
if (dsTexture) {
pipelineDepthStencilFormat = dsTexture->getTexture().GetFormat();
const auto depthStencilTexture{ handleCast<WebGPUTexture>(depthStencilHandle) };
if (depthStencilTexture) {
depthStencilFormat = depthStencilTexture->getTexture().GetFormat();
}
}
}
// TODO: We expected this to be a sane check, however it complains when running shadowtest.
//if (program->fragmentShaderModule != nullptr) {
// FILAMENT_CHECK_POSTCONDITION(!pipelineColorFormats.empty())
// << "Render pipeline with fragment shader must have at least one color target "
// "format.";
//}
wgpu::RenderPipeline pipeline = createWebGPURenderPipeline(mDevice, *program, *vertexBufferInfo,
layout, pipelineState.rasterState, pipelineState.stencilState,
pipelineState.polygonOffset, pipelineState.primitiveType, pipelineColorFormats,
pipelineDepthStencilFormat, pipelineSamples, requestedDepth, requestedStencil);
assert_invariant(pipeline);
mPipelineMap[pipelineKey] = pipeline;
const WebGPUPipelineCache::RenderPipelineRequest pipelineRequest{
.label = program->name,
.vertexShaderModule = program->vertexShaderModule,
.fragmentShaderModule = program->fragmentShaderModule,
.vertexBufferSlots = vertexBufferInfo->getWebGPUSlotBindingInfos(),
.vertexBufferLayouts = vertexBufferInfo->getVertexBufferLayouts(),
.pipelineLayout = layout,
.primitiveType = pipelineState.primitiveType,
.rasterState = pipelineState.rasterState,
.stencilState = pipelineState.stencilState,
.polygonOffset = pipelineState.polygonOffset,
.targetRenderFlags = renderTarget->getTargetFlags(),
.multisampleCount = renderTarget->getSamples(),
.depthStencilFormat = depthStencilFormat,
.colorFormatCount = colorFormatCount,
.colorFormats = colorFormats.data(),
};
wgpu::RenderPipeline const& pipeline{ mPipelineCache.getOrCreateRenderPipeline(
pipelineRequest) };
mRenderPassEncoder.SetPipeline(pipeline);
}
+7 -6
View File
@@ -19,6 +19,9 @@
#include "WebGPURenderTarget.h"
#include "webgpu/WebGPUConstants.h"
#include "webgpu/WebGPUMsaaTextureResolver.h"
#include "webgpu/WebGPUPipelineCache.h"
#include "webgpu/WebGPUPipelineLayoutCache.h"
#include "webgpu/WebGPURenderPassMipmapGenerator.h"
#include <backend/platforms/WebGPUPlatform.h>
@@ -31,7 +34,6 @@
#include <utils/compiler.h>
#include "SpdMipmapGenerator/SpdMipmapGenerator.h"
#include <tsl/robin_map.h>
#include <webgpu/webgpu_cpp.h>
#include <cstdint>
@@ -56,7 +58,7 @@ public:
[[nodiscard]] static Driver* create(WebGPUPlatform& platform, const Platform::DriverConfig& driverConfig) noexcept;
private:
explicit WebGPUDriver(WebGPUPlatform& platform, const Platform::DriverConfig& driverConfig) noexcept;
WebGPUDriver(WebGPUPlatform& platform, const Platform::DriverConfig& driverConfig) noexcept;
[[nodiscard]] ShaderModel getShaderModel() const noexcept final;
[[nodiscard]] ShaderLanguage getShaderLanguage() const noexcept final;
[[nodiscard]] wgpu::Sampler makeSampler(SamplerParams const& params);
@@ -78,10 +80,11 @@ private:
wgpu::CommandBuffer mCommandBuffer = nullptr;
WebGPURenderTarget* mDefaultRenderTarget = nullptr;
WebGPURenderTarget* mCurrentRenderTarget = nullptr;
WebGPUPipelineLayoutCache mPipelineLayoutCache;
WebGPUPipelineCache mPipelineCache;
WebGPURenderPassMipmapGenerator mRenderPassMipmapGenerator;
spd::MipmapGenerator mSpdComputePassMipmapGenerator;
tsl::robin_map<size_t, wgpu::RenderPipeline> mPipelineMap;
WebGPUMsaaTextureResolver mMsaaTextureResolver{};
struct DescriptorSetBindingInfo{
wgpu::BindGroup bindGroup;
@@ -90,8 +93,6 @@ private:
};
std::array<DescriptorSetBindingInfo,MAX_DESCRIPTOR_SET_COUNT> mCurrentDescriptorSets;
[[nodiscard]] size_t computePipelineKey(PipelineState const&, WebGPURenderTarget const*) const;
/*
* Driver interface
*/
@@ -0,0 +1,125 @@
/*
* Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "WebGPUMsaaTextureResolver.h"
#include "WebGPUTexture.h"
#include <utils/Panic.h>
#include <webgpu/webgpu_cpp.h>
namespace filament::backend {
namespace {
void resolveColorTextures(wgpu::CommandEncoder const& commandEncoder,
wgpu::TextureView const& sourceTextureView,
wgpu::TextureView const& destinationTextureView) {
const wgpu::RenderPassColorAttachment colorAttachment{
.view = sourceTextureView,
.depthSlice = wgpu::kDepthSliceUndefined, // being explicit for consistent behavior
.resolveTarget = destinationTextureView,
.loadOp = wgpu::LoadOp::Load,
.storeOp = wgpu::StoreOp::Store,
.clearValue = {}, // being explicit for consistent behavior
};
const wgpu::RenderPassDescriptor renderPassDescriptor{
.label = "resolve_render_pass",
.colorAttachmentCount = 1,
.colorAttachments = &colorAttachment,
.depthStencilAttachment = nullptr, // being explicit for consistent behavior
.occlusionQuerySet = nullptr, // being explicit for consistent behavior
.timestampWrites = nullptr, // being explicit for consistent behavior
};
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
}
} // namespace
void WebGPUMsaaTextureResolver::resolve(ResolveRequest const& request) {
ResolveRequest::TextureInfo const& source{ request.source };
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";
FILAMENT_CHECK_PRECONDITION(
source.texture.GetSampleCount() > 1 && destination.texture.GetSampleCount() == 1)
<< "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";
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(source.texture.GetUsage() & 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";
FILAMENT_CHECK_PRECONDITION(destination.texture.GetUsage() & wgpu::TextureUsage::TextureBinding)
<< "destination texture usage doesn't have wgpu::TextureUsage::TextureBinding";
const wgpu::TextureViewDescriptor sourceTextureViewDescriptor{
.label = "resolve_source_texture_view",
.format = request.viewFormat,
.dimension = source.viewDimension,
.baseMipLevel = source.mipLevel,
.mipLevelCount = 1,
.baseArrayLayer = source.layer,
.arrayLayerCount = 1,
.aspect = source.aspect,
.usage = source.texture.GetUsage(),
};
const wgpu::TextureView sourceTextureView{ source.texture.CreateView(
&sourceTextureViewDescriptor) };
FILAMENT_CHECK_POSTCONDITION(sourceTextureView)
<< "Failed to create wgpu::TextureView sourceTextureView";
const wgpu::TextureViewDescriptor destinationTextureViewDescriptor{
.label = "resolve_destination_texture_view",
.format = request.viewFormat,
.dimension = destination.viewDimension,
.baseMipLevel = destination.mipLevel,
.mipLevelCount = 1,
.baseArrayLayer = destination.layer,
.arrayLayerCount = 1,
.aspect = destination.aspect,
.usage = destination.texture.GetUsage(),
};
const wgpu::TextureView destinationTextureView{ destination.texture.CreateView(
&destinationTextureViewDescriptor) };
FILAMENT_CHECK_POSTCONDITION(destinationTextureView)
<< "Failed to create wgpu::TextureView destinationTextureView.";
if (hasDepth(format)) {
PANIC_PRECONDITION("DEPTH RESOLVE NOT IMPLEMENTED YET");
} else {
resolveColorTextures(request.commandEncoder, sourceTextureView, destinationTextureView);
}
}
} // namespace filament::backend
@@ -0,0 +1,55 @@
/*
* Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef TNT_FILAMENT_BACKEND_WEBGPUMSAATEXTURERESOLVER_H
#define TNT_FILAMENT_BACKEND_WEBGPUMSAATEXTURERESOLVER_H
#include <cstdint>
namespace wgpu {
class CommandEncoder;
class Texture;
enum class TextureAspect : uint32_t;
enum class TextureFormat : uint32_t;
enum class TextureViewDimension : uint32_t;
} // namespace wgpu
namespace filament::backend {
class WebGPUTexture;
class WebGPUMsaaTextureResolver final {
public:
struct ResolveRequest final {
struct TextureInfo final {
wgpu::Texture const& texture;
wgpu::TextureViewDimension viewDimension;
uint8_t mipLevel;
uint8_t layer;
wgpu::TextureAspect aspect;
};
wgpu::CommandEncoder const& commandEncoder;
wgpu::TextureFormat viewFormat;
TextureInfo source;
TextureInfo destination;
};
void resolve(ResolveRequest const&);
};
} // namespace filament::backend
#endif // TNT_FILAMENT_BACKEND_WEBGPUMSAATEXTURERESOLVER_H
@@ -0,0 +1,382 @@
/*
* Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "WebGPUPipelineCache.h"
#include "WebGPUConstants.h"
#include "WebGPUTexture.h"
#include "WebGPUVertexBufferInfo.h"
#include <backend/DriverEnums.h>
#include <backend/TargetBufferInfo.h>
#include <utils/BitmaskEnum.h>
#include <utils/Panic.h>
#include <webgpu/webgpu_cpp.h>
#include <array>
#include <cstdint>
#include <cstring>
namespace filament::backend {
namespace {
[[nodiscard]] constexpr uint8_t toUint8(const bool value) { return value ? 1 : 0; }
[[nodiscard]] constexpr wgpu::PrimitiveTopology toWebGPU(const PrimitiveType primitiveType) {
switch (primitiveType) {
case PrimitiveType::POINTS: return wgpu::PrimitiveTopology::PointList;
case PrimitiveType::LINES: return wgpu::PrimitiveTopology::LineList;
case PrimitiveType::LINE_STRIP: return wgpu::PrimitiveTopology::LineStrip;
case PrimitiveType::TRIANGLES: return wgpu::PrimitiveTopology::TriangleList;
case PrimitiveType::TRIANGLE_STRIP: return wgpu::PrimitiveTopology::TriangleStrip;
}
}
[[nodiscard]] constexpr wgpu::CullMode toWebGPU(const CullingMode cullMode) {
switch (cullMode) {
case CullingMode::NONE: return wgpu::CullMode::None;
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
FILAMENT_CHECK_POSTCONDITION(false)
<< "WebGPU does not support CullingMode::FRONT_AND_BACK";
return wgpu::CullMode::Undefined;
}
}
[[nodiscard]] constexpr wgpu::CompareFunction toWebGPU(const SamplerCompareFunc compareFunction) {
switch (compareFunction) {
case SamplerCompareFunc::LE: return wgpu::CompareFunction::LessEqual;
case SamplerCompareFunc::GE: return wgpu::CompareFunction::GreaterEqual;
case SamplerCompareFunc::L: return wgpu::CompareFunction::Less;
case SamplerCompareFunc::G: return wgpu::CompareFunction::Greater;
case SamplerCompareFunc::E: return wgpu::CompareFunction::Equal;
case SamplerCompareFunc::NE: return wgpu::CompareFunction::NotEqual;
case SamplerCompareFunc::A: return wgpu::CompareFunction::Always;
case SamplerCompareFunc::N: return wgpu::CompareFunction::Never;
}
}
[[nodiscard]] constexpr wgpu::StencilOperation toWebGPU(const StencilOperation stencilOp) {
switch (stencilOp) {
case StencilOperation::KEEP: return wgpu::StencilOperation::Keep;
case StencilOperation::ZERO: return wgpu::StencilOperation::Zero;
case StencilOperation::REPLACE: return wgpu::StencilOperation::Replace;
case StencilOperation::INCR: return wgpu::StencilOperation::IncrementClamp;
case StencilOperation::INCR_WRAP: return wgpu::StencilOperation::IncrementWrap;
case StencilOperation::DECR: return wgpu::StencilOperation::DecrementClamp;
case StencilOperation::DECR_WRAP: return wgpu::StencilOperation::DecrementWrap;
case StencilOperation::INVERT: return wgpu::StencilOperation::Invert;
}
}
[[nodiscard]] constexpr wgpu::BlendOperation toWebGPU(const BlendEquation blendOp) {
switch (blendOp) {
case BlendEquation::ADD: return wgpu::BlendOperation::Add;
case BlendEquation::SUBTRACT: return wgpu::BlendOperation::Subtract;
case BlendEquation::REVERSE_SUBTRACT: return wgpu::BlendOperation::ReverseSubtract;
case BlendEquation::MIN: return wgpu::BlendOperation::Min;
case BlendEquation::MAX: return wgpu::BlendOperation::Max;
}
}
[[nodiscard]] constexpr wgpu::BlendFactor toWebGPU(const BlendFunction blendFunction) {
switch (blendFunction) {
case BlendFunction::ZERO: return wgpu::BlendFactor::Zero;
case BlendFunction::ONE: return wgpu::BlendFactor::One;
case BlendFunction::SRC_COLOR: return wgpu::BlendFactor::Src;
case BlendFunction::ONE_MINUS_SRC_COLOR: return wgpu::BlendFactor::OneMinusSrc;
case BlendFunction::DST_COLOR: return wgpu::BlendFactor::Dst;
case BlendFunction::ONE_MINUS_DST_COLOR: return wgpu::BlendFactor::OneMinusDst;
case BlendFunction::SRC_ALPHA: return wgpu::BlendFactor::SrcAlpha;
case BlendFunction::ONE_MINUS_SRC_ALPHA: return wgpu::BlendFactor::OneMinusSrcAlpha;
case BlendFunction::DST_ALPHA: return wgpu::BlendFactor::DstAlpha;
case BlendFunction::ONE_MINUS_DST_ALPHA: return wgpu::BlendFactor::OneMinusDstAlpha;
case BlendFunction::SRC_ALPHA_SATURATE: return wgpu::BlendFactor::SrcAlphaSaturated;
}
}
} // namespace
WebGPUPipelineCache::WebGPUPipelineCache(wgpu::Device const& device)
: mDevice{ device } {}
wgpu::RenderPipeline const& WebGPUPipelineCache::getOrCreateRenderPipeline(
RenderPipelineRequest const& request) {
RenderPipelineKey key{};
populateKey(request, key);
if (auto iterator{ mRenderPipelines.find(key) }; iterator != mRenderPipelines.end()) {
RenderPipelineCacheEntry& entry{ iterator.value() };
entry.lastUsedFrameCount = mFrameCount;
return entry.pipeline;
}
const wgpu::RenderPipeline pipeline{ createRenderPipeline(request) };
mRenderPipelines.emplace(key, RenderPipelineCacheEntry{
.pipeline = pipeline,
.lastUsedFrameCount = mFrameCount,
});
return mRenderPipelines[key].pipeline;
}
void WebGPUPipelineCache::onFrameEnd() {
++mFrameCount;
removeExpiredPipelines();
}
void WebGPUPipelineCache::populateKey(RenderPipelineRequest const& request,
RenderPipelineKey& outKey) {
outKey.vertexShaderModuleHandle =
request.vertexShaderModule ? request.vertexShaderModule.Get() : nullptr;
outKey.fragmentShaderModuleHandle =
request.fragmentShaderModule ? request.fragmentShaderModule.Get() : nullptr;
outKey.pipelineLayoutHandle = request.pipelineLayout ? request.pipelineLayout.Get() : nullptr;
outKey.depthBias = static_cast<int32_t>(request.polygonOffset.constant);
outKey.depthBiasSlopeScale = request.polygonOffset.slope;
outKey.primitiveType = request.primitiveType;
outKey.stencilFrontCompare = request.stencilState.front.stencilFunc;
outKey.stencilFrontFailOperation = request.stencilState.front.stencilOpStencilFail;
outKey.stencilFrontDepthFailOperation = request.stencilState.front.stencilOpDepthFail;
outKey.stencilFrontPassOperation = request.stencilState.front.stencilOpDepthStencilPass;
outKey.stencilWrite = toUint8(request.stencilState.stencilWrite);
outKey.stencilFrontReadMask = request.stencilState.front.readMask;
outKey.stencilFrontWriteMask = request.stencilState.front.writeMask;
outKey.stencilBackCompare = request.stencilState.back.stencilFunc;
outKey.stencilBackFailOperation = request.stencilState.back.stencilOpStencilFail;
outKey.stencilBackDepthFailOperation = request.stencilState.back.stencilOpDepthFail;
outKey.stencilBackPassOperation = request.stencilState.back.stencilOpDepthStencilPass;
outKey.cullingMode = request.rasterState.culling;
outKey.inverseFrontFaces = toUint8(request.rasterState.inverseFrontFaces);
outKey.depthWriteEnabled = toUint8(request.rasterState.depthWrite);
outKey.depthCompare = request.rasterState.depthFunc;
outKey.depthClamp = toUint8(request.rasterState.depthClamp);
outKey.colorWrite = toUint8(request.rasterState.colorWrite);
outKey.alphaToCoverageEnabled = toUint8(request.rasterState.alphaToCoverage);
outKey.colorBlendOperation = request.rasterState.blendEquationRGB;
outKey.colorBlendSourceFactor = request.rasterState.blendFunctionSrcRGB;
outKey.colorBlendDestinationFactor = request.rasterState.blendFunctionDstRGB;
outKey.alphaBlendOperation = request.rasterState.blendEquationAlpha;
outKey.alphaBlendSourceFactor = request.rasterState.blendFunctionSrcAlpha;
outKey.alphaBlendDestinationFactor = request.rasterState.blendFunctionDstAlpha;
outKey.targetRenderFlags = request.targetRenderFlags;
outKey.multisampleCount = request.multisampleCount;
outKey.depthStencilFormat = request.depthStencilFormat;
assert_invariant(request.colorFormatCount <= MRT::MAX_SUPPORTED_RENDER_TARGET_COUNT);
outKey.colorFormatCount = request.colorFormatCount;
for (size_t colorIndex{ 0 }; colorIndex < request.colorFormatCount; colorIndex++) {
outKey.colorFormats[colorIndex] = request.colorFormats[colorIndex];
}
// vertex buffers...
for (WebGPUVertexBufferInfo::WebGPUSlotBindingInfo const& vertexBufferSlot:
request.vertexBufferSlots) {
assert_invariant(vertexBufferSlot.sourceBufferIndex < MAX_VERTEX_BUFFER_COUNT);
outKey.vertexBuffers[vertexBufferSlot.sourceBufferIndex].stride = vertexBufferSlot.stride;
outKey.vertexBuffers[vertexBufferSlot.sourceBufferIndex].offset =
vertexBufferSlot.bufferOffset;
}
// vertex attributes...
uint8_t currentAttributeIndex{ 0 };
for (size_t bufferIndex{ 0 }; bufferIndex < request.vertexBufferSlots.size(); bufferIndex++) {
wgpu::VertexBufferLayout const& vertexBufferLayout{
request.vertexBufferLayouts[bufferIndex]
};
for (size_t attributeIndex{ 0 }; attributeIndex < vertexBufferLayout.attributeCount;
attributeIndex++) {
assert_invariant(attributeIndex < MAX_VERTEX_ATTRIBUTE_COUNT);
wgpu::VertexAttribute const& vertexAttribute{
vertexBufferLayout.attributes[attributeIndex]
};
outKey.vertexAttributes[currentAttributeIndex].bufferIndex = bufferIndex;
outKey.vertexAttributes[currentAttributeIndex].offset =
static_cast<uint8_t>(vertexAttribute.offset);
outKey.vertexAttributes[currentAttributeIndex].shaderLocation =
static_cast<uint8_t>(vertexAttribute.shaderLocation);
outKey.vertexAttributes[currentAttributeIndex].format = vertexAttribute.format;
currentAttributeIndex++;
}
}
}
wgpu::RenderPipeline WebGPUPipelineCache::createRenderPipeline(
RenderPipelineRequest const& request) {
assert_invariant(request.vertexShaderModule);
wgpu::DepthStencilState depthStencilState{};
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 "
"wgpu::TextureFormat::Undefined.";
depthStencilState.format = request.depthStencilFormat;
if (requestedDepth) {
assert_invariant(hasDepth(depthStencilState.format));
depthStencilState.depthWriteEnabled = request.rasterState.depthWrite;
depthStencilState.depthCompare = toWebGPU(request.rasterState.depthFunc);
depthStencilState.depthBias = static_cast<int32_t>(request.polygonOffset.constant);
depthStencilState.depthBiasSlopeScale = request.polygonOffset.slope;
depthStencilState.depthBiasClamp = 0.0f;
} else {
depthStencilState.depthWriteEnabled = false;
depthStencilState.depthCompare = wgpu::CompareFunction::Undefined;
depthStencilState.depthBias = 0;
depthStencilState.depthBiasSlopeScale = 0.0f;
depthStencilState.depthBiasClamp = 0.0f;
}
if (requestedStencil) {
assert_invariant(hasStencil(depthStencilState.format));
depthStencilState.stencilFront = {
.compare = toWebGPU(request.stencilState.front.stencilFunc),
.failOp = toWebGPU(request.stencilState.front.stencilOpStencilFail),
.depthFailOp = toWebGPU(request.stencilState.front.stencilOpDepthFail),
.passOp = toWebGPU(request.stencilState.front.stencilOpDepthStencilPass),
};
depthStencilState.stencilBack = {
.compare = toWebGPU(request.stencilState.back.stencilFunc),
.failOp = toWebGPU(request.stencilState.back.stencilOpStencilFail),
.depthFailOp = toWebGPU(request.stencilState.back.stencilOpDepthFail),
.passOp = toWebGPU(request.stencilState.back.stencilOpDepthStencilPass),
};
// TODO: should we also consider the back readMask and writeMask?
depthStencilState.stencilReadMask = request.stencilState.front.readMask;
depthStencilState.stencilWriteMask =
request.stencilState.stencilWrite ? request.stencilState.front.writeMask : 0u;
} else {
depthStencilState.stencilFront.compare = wgpu::CompareFunction::Undefined;
depthStencilState.stencilFront.failOp = wgpu::StencilOperation::Keep;
depthStencilState.stencilFront.depthFailOp = wgpu::StencilOperation::Keep;
depthStencilState.stencilFront.passOp = wgpu::StencilOperation::Keep;
depthStencilState.stencilBack = depthStencilState.stencilFront;
depthStencilState.stencilReadMask = 0;
depthStencilState.stencilWriteMask = 0;
}
}
wgpu::RenderPipelineDescriptor pipelineDescriptor{
.label = wgpu::StringView(request.label.c_str_safe()),
.layout = request.pipelineLayout,
.vertex = {
.module = request.vertexShaderModule,
.entryPoint = "main",
// we do not use WebGPU's override constants due to 2 limitations
// (at least at the time of write this):
// 1. they cannot be used for the size of an array, which is needed
// 2. if we pass the WebGPU API (CPU-side) constants not referenced in the
// shader WebGPU fails. This is a problem with how Filament is designed,
// where certain constants may be optimized out of the shader based
// on build configuration, etc.
//
// to bypass these problems, we do not use override constants in the
// WebGPU backend, instead replacing placeholder constants in the shader
// text before creating the shader module (essentially implementing
// override constants ourselves)
.constantCount = 0,
.constants = nullptr,
.bufferCount = request.vertexBufferSlots.size(),
.buffers = request.vertexBufferLayouts,
},
.primitive = {
.topology = toWebGPU(request.primitiveType),
// TODO should we assume some constant format here or is there a way to get
// this from PipelineState somehow or elsewhere?
// Perhaps, cache/assert format from index buffers as they are requested?
.stripIndexFormat = wgpu::IndexFormat::Undefined,
.frontFace = request.rasterState.inverseFrontFaces ? wgpu::FrontFace::CW : wgpu::FrontFace::CCW,
.cullMode = toWebGPU(request.rasterState.culling),
// TODO no depth clamp in WebGPU supported directly. unclippedDepth is close, so we are
// starting there
.unclippedDepth = !request.rasterState.depthClamp &&
mDevice.HasFeature(wgpu::FeatureName::DepthClipControl),
},
.depthStencil = depthOrStencilRequested ? &depthStencilState: nullptr,
.multisample = {
.count = request.multisampleCount,
.mask = 0xFFFFFFFF,
.alphaToCoverageEnabled = (request.multisampleCount > 1) && request.rasterState.alphaToCoverage
},
.fragment = nullptr // will add below if fragment module is included
};
wgpu::FragmentState fragmentState = {};
const wgpu::BlendState blendState {
.color = {
.operation = toWebGPU(request.rasterState.blendEquationRGB),
.srcFactor = toWebGPU(request.rasterState.blendFunctionSrcRGB),
.dstFactor = toWebGPU(request.rasterState.blendFunctionDstRGB)
},
.alpha = {
.operation = toWebGPU(request.rasterState.blendEquationAlpha),
.srcFactor = toWebGPU(request.rasterState.blendFunctionSrcAlpha),
.dstFactor = toWebGPU(request.rasterState.blendFunctionDstAlpha)
}
};
// According to the WebGPU spec, a pipeline cannot have a fragment stage with zero color
// targets. This situation can arise in Filament during depth-only passes (like shadow map
// generation) if the material variant still includes a fragment shader.
//
// To handle this, we check if any color targets are configured for this pipeline. If not, we
// create a pipeline *without* a fragment stage. This makes the pipeline valid for a
// depth-only pass, allowing depth writes to proceed correctly.
if (request.fragmentShaderModule != nullptr && request.colorFormatCount > 0) {
fragmentState.module = request.fragmentShaderModule;
fragmentState.entryPoint = "main";
// see the comment about constants for the vertex state, as the same reasoning applies
// here
fragmentState.constantCount = 0;
fragmentState.constants = nullptr;
fragmentState.targetCount = request.colorFormatCount;
std::array<wgpu::ColorTargetState, MRT::MAX_SUPPORTED_RENDER_TARGET_COUNT> colorTargets {};
assert_invariant(fragmentState.targetCount <= MRT::MAX_SUPPORTED_RENDER_TARGET_COUNT);
for (size_t targetIndex = 0; targetIndex < fragmentState.targetCount; targetIndex++) {
wgpu::ColorTargetState& colorTarget = colorTargets[targetIndex];
colorTarget.format = request.colorFormats[targetIndex];
colorTarget.blend = request.rasterState.hasBlending() ? &blendState : nullptr;
colorTarget.writeMask = request.rasterState.colorWrite ? wgpu::ColorWriteMask::All
: wgpu::ColorWriteMask::None;
}
fragmentState.targets = colorTargets.data();
pipelineDescriptor.fragment = &fragmentState;
}
const wgpu::RenderPipeline pipeline{ mDevice.CreateRenderPipeline(&pipelineDescriptor) };
FILAMENT_CHECK_POSTCONDITION(pipeline)
<< "Failed to create render pipeline for " << pipelineDescriptor.label;
return pipeline;
}
bool WebGPUPipelineCache::RenderPipelineKeyEqual::operator()(RenderPipelineKey const& key1,
RenderPipelineKey const& key2) const {
return 0 == memcmp(reinterpret_cast<void const*>(&key1), reinterpret_cast<void const*>(&key2),
sizeof(key1));
}
void WebGPUPipelineCache::removeExpiredPipelines() {
using Iterator = decltype(mRenderPipelines)::const_iterator;
for (Iterator iterator{ mRenderPipelines.begin() }; iterator != mRenderPipelines.end();) {
RenderPipelineCacheEntry const& entry{ iterator.value() };
if (mFrameCount > (entry.lastUsedFrameCount +
FILAMENT_WEBGPU_RENDER_PIPELINE_EXPIRATION_IN_FRAME_COUNT)) {
// pipeline expired...
iterator = mRenderPipelines.erase(iterator);
} else {
// pipeline not yet expired...
++iterator;
}
}
}
} //namespace filament::backend
@@ -0,0 +1,192 @@
/*
* Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef TNT_FILAMENT_BACKEND_WEBGPUPIPELINECACHE_H
#define TNT_FILAMENT_BACKEND_WEBGPUPIPELINECACHE_H
#include "WebGPUVertexBufferInfo.h"
#include <backend/DriverEnums.h>
#include <backend/TargetBufferInfo.h>
#include <utils/CString.h>
#include <utils/Hash.h>
#include <tsl/robin_map.h>
#include <webgpu/webgpu_cpp.h>
#include <cstdint>
#include <type_traits>
#include <vector>
namespace filament::backend {
class WebGPUPipelineCache final {
public:
struct RenderPipelineRequest final {
utils::CString const& label;
wgpu::ShaderModule const& vertexShaderModule;
wgpu::ShaderModule const& fragmentShaderModule;
std::vector<WebGPUVertexBufferInfo::WebGPUSlotBindingInfo> const& vertexBufferSlots;
wgpu::VertexBufferLayout const* vertexBufferLayouts;
wgpu::PipelineLayout const& pipelineLayout;
const PrimitiveType primitiveType;
RasterState const& rasterState;
StencilState const& stencilState;
PolygonOffset const& polygonOffset;
const TargetBufferFlags targetRenderFlags;
const uint8_t multisampleCount;
const wgpu::TextureFormat depthStencilFormat;
const uint8_t colorFormatCount;
wgpu::TextureFormat const* colorFormats;
};
explicit WebGPUPipelineCache(wgpu::Device const&);
WebGPUPipelineCache(WebGPUPipelineCache const&) = delete;
WebGPUPipelineCache(WebGPUPipelineCache const&&) = delete;
WebGPUPipelineCache& operator=(WebGPUPipelineCache const&) = delete;
WebGPUPipelineCache& operator=(WebGPUPipelineCache const&&) = delete;
[[nodiscard]] wgpu::RenderPipeline const& getOrCreateRenderPipeline(
RenderPipelineRequest const&);
void onFrameEnd();
private:
/**
* Part of the pipeline key specifically about one of the vertex attributes
*/
struct VertexAttribute final { // size : offset (need multiples of 4 bytes for hashing)
uint8_t bufferIndex{ 0 }; // 1 : 0
// this is the webgpu offset, //
// bytes from the start of //
// the vertex data //
// (interleaved offset) //
uint8_t offset{ 0 }; // 1 : 1
uint8_t shaderLocation{ 0 }; // 1 : 2
uint8_t padding { 0 }; // 1 : 3
wgpu::VertexFormat format{ 0 }; // 4 : 4
};
static_assert(sizeof(VertexAttribute) == 8, "VertexAttribute must not have implicit padding.");
static_assert(std::is_trivially_copyable<VertexAttribute>::value,
"VertexAttribute must be a trivially copyable POD for fast hashing.");
/**
* Part of the pipeline key specifically about one of the vertex buffers
*/
struct VertexBuffer final { // size : offset (need multiples of 4 bytes for hashing)
uint8_t stride{ 0 }; // 1 : 0
uint8_t padding[3]{ 0 }; // 3 : 1
// offset in bytes from //
// the start of the //
// (physical) GPU buffer //
// (not logical buffer //
// partition) //
uint32_t offset{ 0 }; // 4 : 4
};
static_assert(sizeof(VertexBuffer) == 8, "VertexBuffer must not have implicit padding.");
static_assert(std::is_trivially_copyable<VertexBuffer>::value,
"VertexAttribute must be a trivially copyable POD for fast hashing.");
/**
* Key designed for efficient hashing and uniquely identifying all the parameters for
* creating a render pipeline.
* The efficient hashing requires a small memory footprint
* (using the smallest representations of enums, just handle instances instead of wrapper class
* 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 RenderPipelineKey final { // size : offset (need multiples of 4 bytes for hashing)
// shaders... //
WGPUShaderModule vertexShaderModuleHandle{ nullptr }; // 8 : 0
WGPUShaderModule fragmentShaderModuleHandle{ nullptr }; // 8 : 8
// vertex attributes... //
VertexAttribute vertexAttributes[MAX_VERTEX_ATTRIBUTE_COUNT]{}; // 128 : 16
VertexBuffer vertexBuffers[MAX_VERTEX_BUFFER_COUNT]{}; // 128 : 144
// pipeline layout... //
WGPUPipelineLayout pipelineLayoutHandle{ nullptr }; // 8 : 272
// general settings... //
int32_t depthBias{ 0 }; // 4 : 280
float depthBiasSlopeScale { 0.0f }; // 4 : 284
PrimitiveType primitiveType{ PrimitiveType::POINTS }; // 1 : 288
// stencil state... //
SamplerCompareFunc stencilFrontCompare{ SamplerCompareFunc::LE }; // 1 : 289
StencilOperation stencilFrontFailOperation{ StencilOperation::KEEP }; // 1 : 290
StencilOperation stencilFrontDepthFailOperation{ StencilOperation::KEEP }; // 1 : 291
StencilOperation stencilFrontPassOperation{ StencilOperation::KEEP }; // 1 : 292
/* bool, 0 -> false, 1 -> true */ uint8_t stencilWrite{ 0 }; // 1 : 293
uint8_t stencilFrontReadMask{ 0 }; // 1 : 294
uint8_t stencilFrontWriteMask{ 0 }; // 1 : 295
SamplerCompareFunc stencilBackCompare{ SamplerCompareFunc::LE }; // 1 : 296
StencilOperation stencilBackFailOperation{ StencilOperation::KEEP }; // 1 : 297
StencilOperation stencilBackDepthFailOperation{ StencilOperation::KEEP }; // 1 : 298
StencilOperation stencilBackPassOperation{ StencilOperation::KEEP }; // 1 : 299
// general rasterization settings... //
CullingMode cullingMode{ CullingMode::NONE }; // 1 : 300
/* bool, 0 -> counter-clockwise, 1 -> clockwise */ uint8_t inverseFrontFaces{ 0 }; // 1 : 301
// rasterization depth state... //
/* bool, 0 -> false, 1 -> true */ uint8_t depthWriteEnabled{ 0 }; // 1 : 302
SamplerCompareFunc depthCompare{ SamplerCompareFunc::LE }; // 1 : 303
/* bool, 0 -> false, 1 -> true */ uint8_t depthClamp{ 0 }; // 1 : 304
// more rasterization flags... //
/* bool, 0 -> false, 1 -> true */ uint8_t colorWrite{ 0 }; // 1 : 305
/* bool, 0 -> false, 1 -> true */ uint8_t alphaToCoverageEnabled{ 0 }; // 1 : 306
// color blending... //
BlendEquation colorBlendOperation{ BlendEquation::ADD }; // 1 : 307
BlendFunction colorBlendSourceFactor{ BlendFunction::ZERO }; // 1 : 308
BlendFunction colorBlendDestinationFactor{ BlendFunction::ZERO }; // 1 : 309
// alpha blending... //
BlendEquation alphaBlendOperation{ BlendEquation::ADD }; // 1 : 310
BlendFunction alphaBlendSourceFactor{ BlendFunction::ZERO }; // 1 : 311
BlendFunction alphaBlendDestinationFactor{ BlendFunction::ZERO }; // 1 : 312
// render targets... //
uint8_t multisampleCount{ 0 }; // 1 : 313
uint8_t colorFormatCount{ 0 }; // 1 : 314
uint8_t padding[5]{ 0 }; // 5 : 319
TargetBufferFlags targetRenderFlags{ TargetBufferFlags::NONE }; // 4 : 320
wgpu::TextureFormat depthStencilFormat { wgpu::TextureFormat::Undefined }; // 4 : 324
wgpu::TextureFormat colorFormats[MRT::MAX_SUPPORTED_RENDER_TARGET_COUNT]{ //
wgpu::TextureFormat::Undefined //
}; // 32 : 328
};
static_assert(sizeof(RenderPipelineKey) == 360,
"RenderPipelineKey must not have implicit padding.");
static_assert(std::is_trivially_copyable<RenderPipelineKey>::value,
"RenderPipelineKey must be a trivially copyable POD for fast hashing.");
struct RenderPipelineKeyEqual {
bool operator()(RenderPipelineKey const&, RenderPipelineKey const&) const;
};
struct RenderPipelineCacheEntry final {
wgpu::RenderPipeline pipeline{ nullptr };
uint64_t lastUsedFrameCount{ 0 };
};
static void populateKey(RenderPipelineRequest const&, RenderPipelineKey& outKey);
[[nodiscard]] wgpu::RenderPipeline createRenderPipeline(RenderPipelineRequest const&);
void removeExpiredPipelines();
wgpu::Device mDevice;
tsl::robin_map<RenderPipelineKey, RenderPipelineCacheEntry,
utils::hash::MurmurHashFn<RenderPipelineKey>, RenderPipelineKeyEqual>
mRenderPipelines{};
uint64_t mFrameCount{ 0 };
};
} // namespace filament::backend
#endif // TNT_FILAMENT_BACKEND_WEBGPUPIPELINECACHE_H
@@ -1,312 +0,0 @@
/*
* Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "WebGPUPipelineCreation.h"
#include "WebGPUProgram.h"
#include "WebGPURenderTarget.h"
#include "WebGPUVertexBufferInfo.h"
#include <backend/DriverEnums.h>
#include <backend/TargetBufferInfo.h>
#include <utils/Panic.h>
#include <utils/debug.h>
#include <webgpu/webgpu_cpp.h>
#include <array>
#include <cstdint>
#include <sstream>
namespace filament::backend {
namespace {
constexpr wgpu::PrimitiveTopology toWebGPU(PrimitiveType primitiveType) {
switch (primitiveType) {
case PrimitiveType::POINTS:
return wgpu::PrimitiveTopology::PointList;
case PrimitiveType::LINES:
return wgpu::PrimitiveTopology::LineList;
case PrimitiveType::LINE_STRIP:
return wgpu::PrimitiveTopology::LineStrip;
case PrimitiveType::TRIANGLES:
return wgpu::PrimitiveTopology::TriangleList;
case PrimitiveType::TRIANGLE_STRIP:
return wgpu::PrimitiveTopology::TriangleStrip;
}
}
constexpr wgpu::CullMode toWebGPU(CullingMode cullMode) {
switch (cullMode) {
case CullingMode::NONE:
return wgpu::CullMode::None;
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
FILAMENT_CHECK_POSTCONDITION(false)
<< "WebGPU does not support CullingMode::FRONT_AND_BACK";
return wgpu::CullMode::Undefined;
}
}
bool hasStencilAspect(wgpu::TextureFormat format) {
switch (format) {
case wgpu::TextureFormat::Stencil8:
case wgpu::TextureFormat::Depth24PlusStencil8:
case wgpu::TextureFormat::Depth32FloatStencil8:
return true;
default:
return false;
}
}
constexpr wgpu::CompareFunction toWebGPU(SamplerCompareFunc compareFunction) {
switch (compareFunction) {
case SamplerCompareFunc::LE:
return wgpu::CompareFunction::LessEqual;
case SamplerCompareFunc::GE:
return wgpu::CompareFunction::GreaterEqual;
case SamplerCompareFunc::L:
return wgpu::CompareFunction::Less;
case SamplerCompareFunc::G:
return wgpu::CompareFunction::Greater;
case SamplerCompareFunc::E:
return wgpu::CompareFunction::Equal;
case SamplerCompareFunc::NE:
return wgpu::CompareFunction::NotEqual;
case SamplerCompareFunc::A:
return wgpu::CompareFunction::Always;
case SamplerCompareFunc::N:
return wgpu::CompareFunction::Never;
}
}
constexpr wgpu::StencilOperation toWebGPU(StencilOperation stencilOp) {
switch (stencilOp) {
case StencilOperation::KEEP:
return wgpu::StencilOperation::Keep;
case StencilOperation::ZERO:
return wgpu::StencilOperation::Zero;
case StencilOperation::REPLACE:
return wgpu::StencilOperation::Replace;
case StencilOperation::INCR:
return wgpu::StencilOperation::IncrementClamp;
case StencilOperation::INCR_WRAP:
return wgpu::StencilOperation::IncrementWrap;
case StencilOperation::DECR:
return wgpu::StencilOperation::DecrementClamp;
case StencilOperation::DECR_WRAP:
return wgpu::StencilOperation::DecrementWrap;
case StencilOperation::INVERT:
return wgpu::StencilOperation::Invert;
}
}
constexpr wgpu::BlendOperation toWebGPU(BlendEquation blendOp) {
switch (blendOp) {
case BlendEquation::ADD:
return wgpu::BlendOperation::Add;
case BlendEquation::SUBTRACT:
return wgpu::BlendOperation::Subtract;
case BlendEquation::REVERSE_SUBTRACT:
return wgpu::BlendOperation::ReverseSubtract;
case BlendEquation::MIN:
return wgpu::BlendOperation::Min;
case BlendEquation::MAX:
return wgpu::BlendOperation::Max;
}
}
constexpr wgpu::BlendFactor toWebGPU(BlendFunction blendFunction) {
switch (blendFunction) {
case BlendFunction::ZERO:
return wgpu::BlendFactor::Zero;
case BlendFunction::ONE:
return wgpu::BlendFactor::One;
case BlendFunction::SRC_COLOR:
return wgpu::BlendFactor::Src;
case BlendFunction::ONE_MINUS_SRC_COLOR:
return wgpu::BlendFactor::OneMinusSrc;
case BlendFunction::DST_COLOR:
return wgpu::BlendFactor::Dst;
case BlendFunction::ONE_MINUS_DST_COLOR:
return wgpu::BlendFactor::OneMinusDst;
case BlendFunction::SRC_ALPHA:
return wgpu::BlendFactor::SrcAlpha;
case BlendFunction::ONE_MINUS_SRC_ALPHA:
return wgpu::BlendFactor::OneMinusSrcAlpha;
case BlendFunction::DST_ALPHA:
return wgpu::BlendFactor::DstAlpha;
case BlendFunction::ONE_MINUS_DST_ALPHA:
return wgpu::BlendFactor::OneMinusDstAlpha;
case BlendFunction::SRC_ALPHA_SATURATE:
return wgpu::BlendFactor::SrcAlphaSaturated;
}
}
}// namespace
wgpu::RenderPipeline createWebGPURenderPipeline(wgpu::Device const& device,
WebGPUProgram const& program, WebGPUVertexBufferInfo const& vertexBufferInfo,
wgpu::PipelineLayout const& layout, RasterState const& rasterState,
StencilState const& stencilState, PolygonOffset const& polygonOffset,
const PrimitiveType primitiveType, std::vector<wgpu::TextureFormat> const& colorFormats,
wgpu::TextureFormat const& depthStencilFormat, const uint8_t samplesCount, const bool requestedDepth, const bool requestedStencil) {
assert_invariant(program.vertexShaderModule);
wgpu::DepthStencilState depthStencilState{};
const bool depthOrStencilRequested = (requestedDepth || requestedStencil);
if (depthOrStencilRequested) {
FILAMENT_CHECK_PRECONDITION(depthStencilFormat != wgpu::TextureFormat::Undefined)
<< "Depth or Stencil requested for pipeline, but depthStencilFormat is "
"wgpu::TextureFormat::Undefined.";
depthStencilState.format = depthStencilFormat;
if (requestedDepth) {
depthStencilState.depthWriteEnabled = rasterState.depthWrite;
depthStencilState.depthCompare = toWebGPU(rasterState.depthFunc);
depthStencilState.depthBias = static_cast<int32_t>(polygonOffset.constant);
depthStencilState.depthBiasSlopeScale = polygonOffset.slope;
depthStencilState.depthBiasClamp = 0.0f;
} else {
depthStencilState.depthWriteEnabled = false;
depthStencilState.depthCompare = wgpu::CompareFunction::Undefined;
depthStencilState.depthBias = 0;
depthStencilState.depthBiasSlopeScale = 0.0f;
depthStencilState.depthBiasClamp = 0.0f;
}
if (hasStencilAspect(depthStencilFormat) && requestedStencil) {
depthStencilState.stencilFront = {
.compare = toWebGPU(stencilState.front.stencilFunc),
.failOp = toWebGPU(stencilState.front.stencilOpStencilFail),
.depthFailOp = toWebGPU(stencilState.front.stencilOpDepthFail),
.passOp = toWebGPU(stencilState.front.stencilOpDepthStencilPass),
};
depthStencilState.stencilBack = {
.compare = toWebGPU(stencilState.back.stencilFunc),
.failOp = toWebGPU(stencilState.back.stencilOpStencilFail),
.depthFailOp = toWebGPU(stencilState.back.stencilOpDepthFail),
.passOp = toWebGPU(stencilState.back.stencilOpDepthStencilPass),
};
depthStencilState.stencilReadMask = stencilState.front.readMask;
depthStencilState.stencilWriteMask = stencilState.stencilWrite ? stencilState.front.writeMask : 0u;
} else {
depthStencilState.stencilFront.compare = wgpu::CompareFunction::Undefined;
depthStencilState.stencilFront.failOp = wgpu::StencilOperation::Keep;
depthStencilState.stencilFront.depthFailOp = wgpu::StencilOperation::Keep;
depthStencilState.stencilFront.passOp = wgpu::StencilOperation::Keep;
depthStencilState.stencilBack = depthStencilState.stencilFront;
depthStencilState.stencilReadMask = 0;
depthStencilState.stencilWriteMask = 0;
}
}
std::stringstream pipelineLabelStream;
pipelineLabelStream << program.name.c_str() << " pipeline";
const auto pipelineLabel = pipelineLabelStream.str();
wgpu::RenderPipelineDescriptor pipelineDescriptor{
.label = wgpu::StringView(pipelineLabel),
.layout = layout,
.vertex = { .module = program.vertexShaderModule,
.entryPoint = "main",
// we do not use WebGPU's override constants due to 2 limitations
// (at least at the time of write this):
// 1. they cannot be used for the size of an array, which is needed
// 2. if we pass the WebGPU API (CPU-side) constants not referenced in the
// shader WebGPU fails. This is a problem with how Filament is designed,
// where certain constants may be optimized out of the shader based
// on build configuration, etc.
//
// to bypass these problems, we do not use override constants in the
// WebGPU backend, instead replacing placeholder constants in the shader
// text before creating the shader module (essentially implementing
// override constants ourselves)
.constantCount = 0,
.constants = nullptr,
.bufferCount = vertexBufferInfo.getVertexBufferLayoutCount(),
.buffers = vertexBufferInfo.getVertexBufferLayouts()
},
.primitive = {
.topology = toWebGPU(primitiveType),
// TODO should we assume some constant format here or is there a way to get
// this from PipelineState somehow or elsewhere?
// Perhaps, cache/assert format from index buffers as they are requested?
.stripIndexFormat = wgpu::IndexFormat::Undefined,
.frontFace = rasterState.inverseFrontFaces ? wgpu::FrontFace::CW : wgpu::FrontFace::CCW,
.cullMode = toWebGPU(rasterState.culling),
// TODO no depth clamp in WebGPU supported directly. unclippedDepth is close, so we are
// starting there
.unclippedDepth = !rasterState.depthClamp &&
device.HasFeature(wgpu::FeatureName::DepthClipControl)
},
.depthStencil = depthOrStencilRequested ? &depthStencilState: nullptr,
.multisample = {
.count = samplesCount,
.mask = 0xFFFFFFFF,
.alphaToCoverageEnabled = (samplesCount > 1) && rasterState.alphaToCoverage
},
.fragment = nullptr // will add below if fragment module is included
};
wgpu::FragmentState fragmentState = {};
std::array<wgpu::ColorTargetState, MRT::MAX_SUPPORTED_RENDER_TARGET_COUNT> colorTargets {};
const wgpu::BlendState blendState {
.color = {
.operation = toWebGPU(rasterState.blendEquationRGB),
.srcFactor = toWebGPU(rasterState.blendFunctionSrcRGB),
.dstFactor = toWebGPU(rasterState.blendFunctionDstRGB)
},
.alpha = {
.operation = toWebGPU(rasterState.blendEquationAlpha),
.srcFactor = toWebGPU(rasterState.blendFunctionSrcAlpha),
.dstFactor = toWebGPU(rasterState.blendFunctionDstAlpha)
}
};
if (program.fragmentShaderModule != nullptr) {
fragmentState.module = program.fragmentShaderModule;
fragmentState.entryPoint = "main";
// see the comment about constants for the vertex state, as the same reasoning applies
// here
fragmentState.constantCount = 0,
fragmentState.constants = nullptr,
fragmentState.targetCount = colorFormats.size();
fragmentState.targets = colorTargets.data();
assert_invariant(fragmentState.targetCount <= MRT::MAX_SUPPORTED_RENDER_TARGET_COUNT);
// We expect a fragment shader implies at least one color target if it outputs color.
// This should be guaranteed by the caller ensuring colorFormats is not empty.
// However, this fails on shadowtest.cpp, TODO investigate why
// assert_invariant(fragmentState.targetCount > 0);
for (size_t targetIndex = 0; targetIndex < fragmentState.targetCount; targetIndex++) {
auto& colorTarget = colorTargets[targetIndex];
colorTarget.format = colorFormats[targetIndex];
colorTarget.blend = rasterState.hasBlending() ? &blendState : nullptr;
colorTarget.writeMask =
rasterState.colorWrite ? wgpu::ColorWriteMask::All : wgpu::ColorWriteMask::None;
}
pipelineDescriptor.fragment = &fragmentState;
}
const wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pipelineDescriptor);
FILAMENT_CHECK_POSTCONDITION(pipeline)
<< "Failed to create render pipeline for " << pipelineDescriptor.label;
return pipeline;
}
}// namespace filament::backend
@@ -1,48 +0,0 @@
/*
* Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef TNT_FILAMENT_BACKEND_WEBGPUPIPELINECREATION_H
#define TNT_FILAMENT_BACKEND_WEBGPUPIPELINECREATION_H
#include <cstdint>
#include <vector>
namespace wgpu {
class Device;
class PipelineLayout;
class RenderPipeline;
enum class TextureFormat : uint32_t;
}// namespace wgpu
namespace filament::backend {
struct PolygonOffset;
enum class PrimitiveType : uint8_t;
struct RasterState;
struct StencilState;
class WebGPUVertexBufferInfo;
class WebGPUProgram;
[[nodiscard]] wgpu::RenderPipeline createWebGPURenderPipeline(wgpu::Device const&,
WebGPUProgram const&, WebGPUVertexBufferInfo const&, wgpu::PipelineLayout const&,
RasterState const&, StencilState const&, PolygonOffset const&, PrimitiveType primitiveType,
std::vector<wgpu::TextureFormat> const& colorFormats, wgpu::TextureFormat const& depthStencilFormat,
uint8_t samplesCount, bool requestedDepth, bool requestedStencil);
}// namespace filament::backend
#endif// TNT_FILAMENT_BACKEND_WEBGPUPIPELINECREATION_H
@@ -0,0 +1,105 @@
/*
* Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "WebGPUPipelineLayoutCache.h"
#include "WebGPUConstants.h"
#include <backend/DriverEnums.h>
#include <utils/CString.h>
#include <utils/Panic.h>
#include <webgpu/webgpu_cpp.h>
#include <array>
#include <cstring>
namespace filament::backend {
WebGPUPipelineLayoutCache::WebGPUPipelineLayoutCache(wgpu::Device const& device)
: mDevice{ device } {}
wgpu::PipelineLayout const& WebGPUPipelineLayoutCache::getOrCreatePipelineLayout(
PipelineLayoutRequest const& request) {
PipelineLayoutKey key{};
populateKey(request, key);
if (auto iterator{ mPipelineLayouts.find(key) }; iterator != mPipelineLayouts.end()) {
PipelineLayoutCacheEntry& entry{ iterator.value() };
entry.lastUsedFrameCount = mFrameCount;
return entry.layout;
}
const wgpu::PipelineLayout layout{ createPipelineLayout(request) };
mPipelineLayouts.emplace(key, PipelineLayoutCacheEntry{
.layout = layout,
.lastUsedFrameCount = mFrameCount,
});
return mPipelineLayouts[key].layout;
}
void WebGPUPipelineLayoutCache::onFrameEnd() {
++mFrameCount;
removeExpiredPipelineLayouts();
}
void WebGPUPipelineLayoutCache::populateKey(PipelineLayoutRequest const& request,
PipelineLayoutKey& outKey) {
outKey.bindGroupLayoutCount = static_cast<uint8_t>(request.bindGroupLayoutCount);
for (size_t bindGroupIndex{ 0 }; bindGroupIndex < request.bindGroupLayoutCount;
++bindGroupIndex) {
outKey.bindGroupLayoutHandles[bindGroupIndex] =
request.bindGroupLayouts[bindGroupIndex]
? request.bindGroupLayouts[bindGroupIndex].Get()
: nullptr;
}
}
wgpu::PipelineLayout WebGPUPipelineLayoutCache::createPipelineLayout(
PipelineLayoutRequest const& request) {
const wgpu::PipelineLayoutDescriptor descriptor{
.label = wgpu::StringView(request.label.c_str_safe()),
.bindGroupLayoutCount = request.bindGroupLayoutCount,
.bindGroupLayouts = request.bindGroupLayouts.data(),
// TODO investigate immediateDataRangeByteSize
};
const wgpu::PipelineLayout layout{ mDevice.CreatePipelineLayout(&descriptor) };
FILAMENT_CHECK_POSTCONDITION(layout)
<< "Failed to create pipeline layout " << descriptor.label << "?";
return layout;
}
bool WebGPUPipelineLayoutCache::PipelineLayoutKeyEqual::operator()(PipelineLayoutKey const& key1,
PipelineLayoutKey const& key2) const {
return 0 == memcmp(reinterpret_cast<void const*>(&key1), reinterpret_cast<void const*>(&key2),
sizeof(key1));
}
void WebGPUPipelineLayoutCache::removeExpiredPipelineLayouts() {
using Iterator = decltype(mPipelineLayouts)::const_iterator;
for (Iterator iterator{ mPipelineLayouts.begin() }; iterator != mPipelineLayouts.end();) {
PipelineLayoutCacheEntry const& entry{ iterator.value() };
if (mFrameCount > (entry.lastUsedFrameCount +
FILAMENT_WEBGPU_PIPELINE_LAYOUT_EXPIRATION_IN_FRAME_COUNT)) {
// pipeline layout expired...
iterator = mPipelineLayouts.erase(iterator);
} else {
// pipeline layout not yet expired...
++iterator;
}
}
}
} // namespace filament::backend
@@ -0,0 +1,96 @@
/*
* Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef TNT_FILAMENT_BACKEND_WEBGPUPIPELINELAYOUTCACHE_H
#define TNT_FILAMENT_BACKEND_WEBGPUPIPELINELAYOUTCACHE_H
#include <backend/DriverEnums.h>
#include <utils/CString.h>
#include <utils/Hash.h>
#include <tsl/robin_map.h>
#include <webgpu/webgpu_cpp.h>
#include <array>
#include <cstdint>
#include <type_traits>
namespace filament::backend {
class WebGPUPipelineLayoutCache final {
public:
struct PipelineLayoutRequest final {
utils::CString const& label;
std::array<wgpu::BindGroupLayout, MAX_DESCRIPTOR_SET_COUNT> const& bindGroupLayouts;
size_t bindGroupLayoutCount;
};
explicit WebGPUPipelineLayoutCache(wgpu::Device const&);
WebGPUPipelineLayoutCache(WebGPUPipelineLayoutCache const&) = delete;
WebGPUPipelineLayoutCache(WebGPUPipelineLayoutCache const&&) = delete;
WebGPUPipelineLayoutCache& operator=(WebGPUPipelineLayoutCache const&) = delete;
WebGPUPipelineLayoutCache& operator=(WebGPUPipelineLayoutCache const&&) = delete;
[[nodiscard]] wgpu::PipelineLayout const& getOrCreatePipelineLayout(
PipelineLayoutRequest const&);
void onFrameEnd();
private:
/**
* Key designed for efficient hashing and uniquely identifying all the parameters for
* creating a pipeline layout.
* The efficient hashing requires a small memory footprint
* (using the smallest representations of enums, just handle instances instead of wrapper class
* 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
};
static_assert(sizeof(PipelineLayoutKey) == 40,
"PipelineLayoutKey must not have implicit padding.");
static_assert(std::is_trivially_copyable<PipelineLayoutKey>::value,
"PipelineLayoutKey must be a trivially copyable POD for fast hashing.");
struct PipelineLayoutKeyEqual {
bool operator()(PipelineLayoutKey const&, PipelineLayoutKey const&) const;
};
struct PipelineLayoutCacheEntry final {
wgpu::PipelineLayout layout{ nullptr };
uint64_t lastUsedFrameCount{ 0 };
};
static void populateKey(PipelineLayoutRequest const&, PipelineLayoutKey& outKey);
[[nodiscard]] wgpu::PipelineLayout createPipelineLayout(PipelineLayoutRequest const&);
void removeExpiredPipelineLayouts();
wgpu::Device mDevice;
tsl::robin_map<PipelineLayoutKey, PipelineLayoutCacheEntry,
utils::hash::MurmurHashFn<PipelineLayoutKey>, PipelineLayoutKeyEqual>
mPipelineLayouts{};
uint64_t mFrameCount{ 0 };
};
} // namespace filament::backend
#endif // TNT_FILAMENT_BACKEND_WEBGPUPIPELINELAYOUTCACHE_H
@@ -234,7 +234,7 @@ namespace {
FWGPU_LOGD << descriptor.label << " compiled successfully";
#endif
}),
SHADER_COMPILATION_TIMEOUT_NANOSECONDS);
FILAMENT_WEBGPU_SHADER_COMPILATION_TIMEOUT_NANOSECONDS);
switch (waitResult) {
case wgpu::WaitStatus::Success:
break;
@@ -56,7 +56,7 @@ private:
void generateMipmap(wgpu::CommandEncoder const&, wgpu::Texture const&,
wgpu::RenderPipeline const&, uint32_t layer, uint32_t mipLevel);
wgpu::Device const& mDevice;
wgpu::Device mDevice;
const wgpu::Sampler mPreviousMipLevelSampler{ nullptr };
const wgpu::ShaderModule mShaderModule{ nullptr };
const wgpu::BindGroupLayout mTextureBindGroupLayout{ nullptr };
@@ -16,7 +16,11 @@
#include "WebGPURenderTarget.h"
#include "WebGPUTexture.h"
#include "DriverBase.h"
#include <backend/DriverEnums.h>
#include <backend/Handle.h>
#include <backend/TargetBufferInfo.h>
#include <private/backend/BackendUtils.h>
@@ -26,14 +30,97 @@
#include <webgpu/webgpu_cpp.h>
#include <algorithm>
#include <array>
#include <cstdint>
#include <functional>
#include <string_view>
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.
*/
[[nodiscard]] uint8_t createMsaaSidecarTextures(const uint8_t renderTargetSampleCount,
const TargetBufferFlags targetFlags, MRT const& colorAttachments,
TargetBufferInfo const& depthAttachment, TargetBufferInfo const& stencilAttachment,
std::function<WebGPUTexture*(const Handle<HwTexture>)> const& getWebGPUTexture,
wgpu::Device const& device) {
struct Target final {
std::string_view name;
TargetBufferFlags flag{ TargetBufferFlags::NONE };
size_t colorIndex{ 0 };
};
// assuming 8 colors. hopefully the following static_assert will get tripped if another
// color ever gets added
static_assert(
(TargetBufferFlags::COLOR0 | TargetBufferFlags::COLOR1 | TargetBufferFlags::COLOR2 |
TargetBufferFlags::COLOR3 | TargetBufferFlags::COLOR4 |
TargetBufferFlags::COLOR5 | TargetBufferFlags::COLOR6 |
TargetBufferFlags::COLOR7) == TargetBufferFlags::COLOR_ALL);
const static std::array TARGETS{
Target{ .name = "COLOR0", .flag = TargetBufferFlags::COLOR0, .colorIndex = 0 },
Target{ .name = "COLOR1", .flag = TargetBufferFlags::COLOR1, .colorIndex = 1 },
Target{ .name = "COLOR2", .flag = TargetBufferFlags::COLOR2, .colorIndex = 2 },
Target{ .name = "COLOR3", .flag = TargetBufferFlags::COLOR3, .colorIndex = 3 },
Target{ .name = "COLOR4", .flag = TargetBufferFlags::COLOR4, .colorIndex = 4 },
Target{ .name = "COLOR5", .flag = TargetBufferFlags::COLOR5, .colorIndex = 5 },
Target{ .name = "COLOR6", .flag = TargetBufferFlags::COLOR6, .colorIndex = 6 },
Target{ .name = "COLOR7", .flag = TargetBufferFlags::COLOR7, .colorIndex = 7 },
Target{ .name = "DEPTH", .flag = TargetBufferFlags::DEPTH },
Target{ .name = "STENCIL", .flag = TargetBufferFlags::STENCIL },
};
#ifndef NDEBUG
for (auto const& target: TARGETS) {
assert_invariant(target.colorIndex <= MRT::MAX_SUPPORTED_RENDER_TARGET_COUNT &&
"color index is >= MRT::MAX_SUPPORTED_RENDER_TARGET_COUNT, which should "
"not be possible and could lead to us indexing into the colorAttachments "
"array beyond its bounds.");
}
#endif
bool firstAttachment{ true };
uint8_t sampleCountPerAttachment{ 0 };
for (auto const& target: TARGETS) {
if (any(targetFlags & target.flag)) {
const Handle<HwTexture> textureHandle{
target.flag == TargetBufferFlags::DEPTH
? depthAttachment.handle
: (target.flag == TargetBufferFlags::STENCIL
? stencilAttachment.handle
: colorAttachments[target.colorIndex].handle)
};
WebGPUTexture* const texture{ getWebGPUTexture(textureHandle) };
assert_invariant(texture != nullptr && "target flag indicate the use of an attachment "
"for which we do not have a texture?");
if (firstAttachment) {
sampleCountPerAttachment = texture->samples;
firstAttachment = false;
}
FILAMENT_CHECK_PRECONDITION(texture->samples == sampleCountPerAttachment)
<< target.name << " attachment texture has " << +texture->samples
<< " but the other attachment(s) have " << +sampleCountPerAttachment;
if (renderTargetSampleCount > 1 && sampleCountPerAttachment == 1) {
texture->createMsaaSidecarTextureIfNotAlreadyCreated(renderTargetSampleCount,
device);
}
}
}
assert_invariant(sampleCountPerAttachment);
return sampleCountPerAttachment;
}
} // namespace
WebGPURenderTarget::WebGPURenderTarget(const uint32_t width, const uint32_t height,
const uint8_t samples, const uint8_t layerCount, MRT const& colorAttachmentsMRT,
Attachment const& depthAttachmentInfo, Attachment const& stencilAttachmentInfo,
TargetBufferFlags const& targetFlags)
TargetBufferFlags const& targetFlags,
std::function<WebGPUTexture*(const Handle<HwTexture>)> const& getWebGPUTexture,
wgpu::Device const& device)
: HwRenderTarget{ width, height },
mDefaultRenderTarget{ false },
mTargetFlags{ targetFlags },
@@ -41,8 +128,11 @@ WebGPURenderTarget::WebGPURenderTarget(const uint32_t width, const uint32_t heig
mLayerCount{ layerCount },
mColorAttachments{ colorAttachmentsMRT },
mDepthAttachment{ depthAttachmentInfo },
mStencilAttachment{ stencilAttachmentInfo } {
// TODO consider making this an array
mStencilAttachment{ stencilAttachmentInfo },
mSampleCountPerAttachment{ createMsaaSidecarTextures(mSamples, mTargetFlags,
mColorAttachments, mDepthAttachment, mStencilAttachment, getWebGPUTexture, device) } {
// TODO consider possibly making this an array (that would avoid a heap allocation, but the
// concern is the size limitation on the handle itself)
mColorAttachmentDesc.reserve(MRT::MAX_SUPPORTED_RENDER_TARGET_COUNT);
}
@@ -52,8 +142,8 @@ WebGPURenderTarget::WebGPURenderTarget()
mDefaultRenderTarget{ true },
mTargetFlags{ TargetBufferFlags::NONE },
mSamples{ 1 },
mLayerCount{ 1 }
{}
mLayerCount{ 1 },
mSampleCountPerAttachment{ 1 } {}
wgpu::LoadOp WebGPURenderTarget::getLoadOperation(RenderPassParams const& params,
const TargetBufferFlags bufferToOperateOn) {
@@ -77,8 +167,35 @@ wgpu::StoreOp WebGPURenderTarget::getStoreOperation(RenderPassParams const& para
void WebGPURenderTarget::setUpRenderPassAttachments(wgpu::RenderPassDescriptor& outDescriptor,
RenderPassParams const& params, wgpu::TextureView const& defaultColorTextureView,
wgpu::TextureView const& defaultDepthStencilTextureView,
wgpu::TextureView const* customColorTextureViews, uint32_t customColorTextureViewCount,
wgpu::TextureView const& customDepthStencilTextureView) {
wgpu::TextureView const* customColorTextureViews,
wgpu::TextureView const* customColorMsaaSidecarTextureViews,
uint32_t customColorTextureViewCount,
wgpu::TextureView const& customDepthStencilTextureView,
wgpu::TextureView const& customDepthStencilMsaaSidecarTextureView) {
const bool hasMsaaSidecars{ (customColorTextureViewCount > 0 &&
customColorMsaaSidecarTextureViews[0]) ||
customDepthStencilMsaaSidecarTextureView };
// either all the textures have MSAA sidecars or none of them do
if (hasMsaaSidecars) {
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 "
"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 "
"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 "
"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 "
"the depth/stencil texture does.";
}
mColorAttachmentDesc.clear();
const bool hasDepth = any(mTargetFlags & TargetBufferFlags::DEPTH);
@@ -104,9 +221,10 @@ void WebGPURenderTarget::setUpRenderPassAttachments(wgpu::RenderPassDescriptor&
} else {
for (uint32_t i = 0; i < customColorTextureViewCount; ++i) {
if (customColorTextureViews[i]) {
mColorAttachmentDesc.push_back({ .view = customColorTextureViews[i],
.resolveTarget =
nullptr, // We handle MSAA on the WebGPU driver's resolve function
const wgpu::TextureView msaaSidecar{ customColorMsaaSidecarTextureViews[i] };
mColorAttachmentDesc.push_back({
.view = hasMsaaSidecars ? msaaSidecar : customColorTextureViews[i],
.resolveTarget = hasMsaaSidecars ? customColorTextureViews[i] : nullptr,
.loadOp =
WebGPURenderTarget::getLoadOperation(params, getTargetBufferFlagsAt(i)),
.storeOp = WebGPURenderTarget::getStoreOperation(params,
@@ -132,7 +250,8 @@ void WebGPURenderTarget::setUpRenderPassAttachments(wgpu::RenderPassDescriptor&
assert_invariant(depthStencilViewToUse);
} else {
if (customDepthStencilTextureView) {
depthStencilViewToUse = customDepthStencilTextureView;
depthStencilViewToUse = hasMsaaSidecars ? customDepthStencilMsaaSidecarTextureView
: customDepthStencilTextureView;
}
}
@@ -17,6 +17,8 @@
#ifndef TNT_FILAMENT_BACKEND_WEBGPUHANDLES_H
#define TNT_FILAMENT_BACKEND_WEBGPUHANDLES_H
#include "WebGPUTexture.h"
#include "DriverBase.h"
#include <backend/DriverEnums.h>
#include <backend/TargetBufferInfo.h>
@@ -24,6 +26,7 @@
#include <webgpu/webgpu_cpp.h>
#include <cstdint>
#include <functional>
#include <vector>
namespace filament::backend {
@@ -34,24 +37,27 @@ 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);
Attachment const& stencilAttachment, TargetBufferFlags const& targetFlags,
std::function<WebGPUTexture*(const Handle<HwTexture>)> const&, wgpu::Device const&);
// Default constructor for the default render target
WebGPURenderTarget();
void setUpRenderPassAttachments(
wgpu::RenderPassDescriptor& outDescriptor,
void setUpRenderPassAttachments(wgpu::RenderPassDescriptor& outDescriptor,
RenderPassParams const& params,
// For default render target:
wgpu::TextureView const& defaultColorTextureView,
wgpu::TextureView const& defaultDepthStencilTextureView,
// For custom render targets:
wgpu::TextureView const* customColorTextureViews, // Array of views
wgpu::TextureView const* customColorTextureViews, // Array of views
wgpu::TextureView const* customColorMsaaSidecarTextureViews, // nullptrs if N/A
uint32_t customColorTextureViewCount,
wgpu::TextureView const& customDepthStencilTextureView);
wgpu::TextureView const& customDepthStencilTextureView,
wgpu::TextureView const& customDepthStencilMsaaSidecarTextureView /* nullptr if N/A */);
[[nodiscard]] bool isDefaultRenderTarget() const { return mDefaultRenderTarget; }
[[nodiscard]] uint8_t getSamples() const { return mSamples; }
[[nodiscard]] uint8_t getSampleCountPerAttachment() const { return mSampleCountPerAttachment; }
[[nodiscard]] uint8_t getLayerCount() const { return mLayerCount; }
[[nodiscard]] MRT const& getColorAttachmentInfos() const { return mColorAttachments; }
@@ -78,6 +84,8 @@ private:
Attachment mDepthAttachment{};
Attachment mStencilAttachment{};
uint8_t mSampleCountPerAttachment = 0;
// Cached descriptors for the render pass
std::vector<wgpu::RenderPassColorAttachment> mColorAttachmentDesc;
wgpu::RenderPassDepthStencilAttachment mDepthStencilAttachmentDesc{};
+77 -11
View File
@@ -402,7 +402,21 @@ WebGPUTexture::WebGPUTexture(WebGPUTexture const* src, const uint8_t baseLevel,
mDefaultMipLevel{ baseLevel },
mDefaultBaseArrayLayer{ src->mArrayLayerCount },
mDefaultTextureView{ makeTextureView(mDefaultMipLevel, levelCount, 0, mDefaultBaseArrayLayer,
src->target) } {}
src->target) },
mMsaaSidecarTexture{ src->mMsaaSidecarTexture } {}
wgpu::Texture const& WebGPUTexture::getMsaaSidecarTexture(const uint8_t sampleCount) const {
if (mMsaaSidecarTexture == nullptr) {
return mMsaaSidecarTexture; // nullptr (no such sidecar)
}
FILAMENT_CHECK_PRECONDITION(sampleCount == mMsaaSidecarTexture.GetSampleCount())
<< "The MSAA sidecar texture has a different sample count ("
<< mMsaaSidecarTexture.GetSampleCount() << ") than requested (" << +sampleCount
<< "). Note that this restriction was written when WebGPU only supported msaa "
"textures with 4 samples. If that has changed, this implementation should be "
"updated (e.g. map of sidecar textures by sampleCount or something).";
return mMsaaSidecarTexture;
}
bool WebGPUTexture::supportsMultipleMipLevelsViaStorageBinding(const wgpu::TextureFormat format) {
return storageBindingCompatibleFormatForViewFormat(format) != wgpu::TextureFormat::Undefined;
@@ -417,6 +431,68 @@ wgpu::TextureView WebGPUTexture::getOrMakeTextureView(const uint8_t mipLevel,
return makeTextureView(mipLevel, 1, arrayLayer, 1, target);
}
void WebGPUTexture::createMsaaSidecarTextureIfNotAlreadyCreated(const uint8_t samples,
wgpu::Device const& device) {
FILAMENT_CHECK_PRECONDITION(samples > 1) << "Requesting to create a MSAA sidecar texture for "
<< +samples << " samples? Invalid request.";
if (mMsaaSidecarTexture) {
FILAMENT_CHECK_PRECONDITION(mMsaaSidecarTexture.GetSampleCount() == samples)
<< "An MSAA sidecar texture has already been created for this texture, but with a "
"different sample count ("
<< mMsaaSidecarTexture.GetSampleCount() << ") than requested (" << +samples
<< "). Note that this restriction was written when WebGPU only supported msaa "
"textures with 4 samples. If that has changed, this implementation should be "
"updated (e.g. map of sidecar textures by sampleCount or something).";
return; // we already have the sidecar created
}
const wgpu::TextureDescriptor descriptor{
.label = "msaa_sidecar_texture",
.usage = mTexture.GetUsage(),
.dimension = mTexture.GetDimension(),
.size = {
.width = mTexture.GetWidth(),
.height = mTexture.GetHeight(),
.depthOrArrayLayers = mTexture.GetDepthOrArrayLayers(),
},
.format = mTexture.GetFormat(),
.mipLevelCount = mTexture.GetMipLevelCount(),
.sampleCount = samples,
.viewFormatCount = 1,
.viewFormats = &mViewFormat,
};
mMsaaSidecarTexture = device.CreateTexture(&descriptor);
FILAMENT_CHECK_POSTCONDITION(mMsaaSidecarTexture) << "Failed to create MSAA sidecar texture";
}
wgpu::TextureView WebGPUTexture::makeMsaaSidecarTextureViewIfTextureSidecarExists(
const uint8_t samples, const uint8_t mipLevel, const uint32_t arrayLayer) const {
if (mMsaaSidecarTexture == nullptr) {
return nullptr;
}
FILAMENT_CHECK_PRECONDITION(mMsaaSidecarTexture.GetSampleCount() == samples)
<< "An MSAA sidecar texture has already been created for this texture, but with a "
"different sample count ("
<< mMsaaSidecarTexture.GetSampleCount() << ") than requested for view (" << +samples
<< "). Note that this restriction was written when WebGPU only supported msaa "
"textures with 4 samples. If that has changed, this implementation should be "
"updated (e.g. map of sidecar textures by sampleCount or something).";
const wgpu::TextureViewDescriptor descriptor{
.label = "msaa_sidecar_texture_view",
.format = mViewFormat,
.dimension = mDimension,
.baseMipLevel = mipLevel,
.mipLevelCount = 1,
.baseArrayLayer = arrayLayer,
.arrayLayerCount = 1,
.aspect = mAspect,
.usage = mViewUsage,
};
const wgpu::TextureView textureView{ mMsaaSidecarTexture.CreateView(&descriptor) };
FILAMENT_CHECK_POSTCONDITION(mMsaaSidecarTexture)
<< "Failed to create MSAA sidecar texture view (" << +samples << " samples)";
return textureView;
}
wgpu::TextureFormat WebGPUTexture::fToWGPUTextureFormat(TextureFormat const& fFormat) {
switch (fFormat) {
case TextureFormat::R8: return wgpu::TextureFormat::R8Unorm;
@@ -591,16 +667,6 @@ wgpu::TextureFormat WebGPUTexture::fToWGPUTextureFormat(TextureFormat const& fFo
wgpu::TextureView WebGPUTexture::makeTextureView(const uint8_t& baseLevel,
const uint8_t& levelCount, const uint32_t& baseArrayLayer, const uint32_t& arrayLayerCount,
const SamplerType samplerType) const noexcept {
#if FWGPU_ENABLED(FWGPU_DEBUG_VALIDATION)
if (baseLevel > 0 && mMipmapGenerationStrategy == MipmapGenerationStrategy::NONE) {
FWGPU_LOGW << "Trying to make a texture view into a level ("
<< static_cast<uint32_t>(baseLevel)
<< ") for which we cannot generate mip levels. SamplerType "
<< to_string(samplerType) << " WebGPU view format "
<< webGPUTextureFormatToString(mViewFormat) << " samples "
<< static_cast<uint32_t>(samples);
}
#endif
const wgpu::TextureViewDescriptor textureViewDescriptor{
.label = getUserTextureViewLabel(target),
.format = mViewFormat,
@@ -26,6 +26,20 @@
namespace filament::backend {
[[nodiscard]] constexpr bool hasStencil(const wgpu::TextureFormat textureFormat) {
return textureFormat == wgpu::TextureFormat::Depth24PlusStencil8 ||
textureFormat == wgpu::TextureFormat::Depth32FloatStencil8 ||
textureFormat == wgpu::TextureFormat::Stencil8;
}
[[nodiscard]] constexpr bool hasDepth(const wgpu::TextureFormat textureFormat) {
return textureFormat == wgpu::TextureFormat::Depth16Unorm ||
textureFormat == wgpu::TextureFormat::Depth32Float ||
textureFormat == wgpu::TextureFormat::Depth24Plus ||
textureFormat == wgpu::TextureFormat::Depth24PlusStencil8 ||
textureFormat == wgpu::TextureFormat::Depth32FloatStencil8;
}
class WebGPUTexture : public HwTexture {
public:
enum class MipmapGenerationStrategy : uint8_t {
@@ -47,6 +61,8 @@ public:
[[nodiscard]] wgpu::Texture const& getTexture() const { return mTexture; }
[[nodiscard]] wgpu::Texture const& getMsaaSidecarTexture(uint8_t sampleCount) const;
[[nodiscard]] wgpu::TextureView const& getDefaultTextureView() const {
return mDefaultTextureView;
}
@@ -62,6 +78,33 @@ public:
return mMipmapGenerationStrategy;
}
/**
* Creates the MSAA sidecar texture if it has not already been created.
* If the sidecar already exists with a different number of samples this will panic
* (at the time of writing this WebGPU only supports MSAA with 4 samples. If this changes, then
* multiple sidecars with different number of samples could be supported if needed, but that
* level of complexity is not warranted at this time).
* Additionally, if samples is <= 1 this will panic as well, as that is not an MSAA texture.
* @param samples The number of samples the texture will have
*/
void createMsaaSidecarTextureIfNotAlreadyCreated(uint8_t samples, wgpu::Device const&);
/**
* @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
* (this view will only have one mip level)
* @param arrayLayer The layer into the underyling texture for which this view will reference
* (this view will only have one layer)
* @return A texture view for the MSAA sidecar texture
*/
wgpu::TextureView makeMsaaSidecarTextureViewIfTextureSidecarExists(uint8_t samples,
uint8_t mipLevel, uint32_t arrayLayer) const;
/**
* @return nullptr if a MSAA sidecar texture is not appliable, otherwise a view to one
*/
[[nodiscard]] wgpu::TextureView makeMsaaSidecarTextureView(wgpu::Texture const&, uint8_t mipLevel, uint32_t arrayLayer) const;
[[nodiscard]] static wgpu::TextureFormat fToWGPUTextureFormat(
filament::backend::TextureFormat const& fFormat);
@@ -96,6 +139,11 @@ private:
uint32_t mDefaultMipLevel = 0;
uint32_t mDefaultBaseArrayLayer = 0;
wgpu::TextureView mDefaultTextureView = nullptr;
// At the time of writing this, WebGPU only supported 4 samples in a multi-sampled texture.
// If that has changed, then consider updating the implementation to have a map of msaa textures
// by sampleCount or something like that.
// For now that complexity and cost is not warranted due to WebGPU's restrictions.
wgpu::Texture mMsaaSidecarTexture = nullptr;
[[nodiscard]] wgpu::TextureView makeTextureView(const uint8_t& baseLevel,
const uint8_t& levelCount, const uint32_t& baseArrayLayer,
@@ -486,7 +486,8 @@ struct AdapterDetailsHash final {
for (size_t i = 0; i < futures.size(); i++) {
wgpu::RequestAdapterOptions const& options = requests[i];
wgpu::Future& future = futures[i];
wgpu::WaitStatus status = instance.WaitAny(future, REQUEST_ADAPTER_TIMEOUT_NANOSECONDS);
wgpu::WaitStatus status =
instance.WaitAny(future, FILAMENT_WEBGPU_REQUEST_ADAPTER_TIMEOUT_NANOSECONDS);
FILAMENT_CHECK_POSTCONDITION(status != wgpu::WaitStatus::TimedOut)
<< "Timed out requesting a WebGPU adapter with options "
<< adapterOptionsToString(options);
@@ -658,7 +659,7 @@ wgpu::Device WebGPUPlatform::requestDevice(wgpu::Adapter const& adapter) {
assert_invariant(status == wgpu::RequestDeviceStatus::Success);
device = readyDevice;
}),
REQUEST_DEVICE_TIMEOUT_NANOSECONDS);
FILAMENT_WEBGPU_REQUEST_DEVICE_TIMEOUT_NANOSECONDS);
FILAMENT_CHECK_POSTCONDITION(status != wgpu::WaitStatus::TimedOut)
<< "Failed to request a WebGPU device due to a timeout.";
FILAMENT_CHECK_POSTCONDITION(status != wgpu::WaitStatus::Error)
@@ -26,7 +26,6 @@
#include <vector>
// Platform specific includes and defines
#include <Cocoa/Cocoa.h>
#import <QuartzCore/CAMetalLayer.h>
/**
+9 -4
View File
@@ -120,8 +120,11 @@ public:
}
/**
* @deprecated Use transform() instead
* @see transform()
* Transform a Box by a linear transform and a translation.
*
* @param m a linear transform matrix
* @param box the box to transform
* @return the bounding box of the transformed box
*/
friend Box rigidTransform(Box const& box, const math::mat4f& m) noexcept {
return transform(m.upperLeft(), m[3].xyz, box);
@@ -235,8 +238,10 @@ struct UTILS_PUBLIC Aabb {
}
/**
* @deprecated Use transform() instead
* @see transform()
* Applies an affine transformation to the AABB.
*
* @param m the affine transformation to apply
* @return the bounding box of the transformed box
*/
Aabb transform(const math::mat4f& m) const noexcept {
return transform(m.upperLeft(), m[3].xyz, *this);
+16 -5
View File
@@ -339,13 +339,24 @@ public:
static math::float4 getColorEstimate(const math::float3 sh[UTILS_NONNULL 9],
math::float3 direction) noexcept;
/** @deprecated use static versions instead */
UTILS_DEPRECATED
/**
* Helper to estimate the direction of the dominant light in the environment represented by
* spherical harmonics.
* Spherical harmonics must be set in the Builder or the result is undefined.
* @see getDirectionEstimate(const math::float3)
* @see Builder::irradiance(uint8_t, math::float3 const*)
* @see Builder::radiance(uint8_t, math::float3 const*)
*/
math::float3 getDirectionEstimate() const noexcept;
/** @deprecated use static versions instead */
UTILS_DEPRECATED
/**
* Helper to estimate the color and relative intensity of the environment represented by
* spherical harmonics in a given direction.
* Spherical harmonics must be set in the Builder or the result is undefined.
* @see getColorEstimate(const math::float3, math::float3)
* @see Builder::irradiance(uint8_t, math::float3 const*)
* @see Builder::radiance(uint8_t, math::float3 const*)
*/
math::float4 getColorEstimate(math::float3 direction) const noexcept;
protected:
+2 -1
View File
@@ -57,7 +57,8 @@ class UTILS_PUBLIC MaterialInstance : public FilamentAPI {
public:
using CullingMode = backend::CullingMode;
using TransparencyMode = TransparencyMode;
// ReSharper disable once CppRedundantQualifier
using TransparencyMode = filament::TransparencyMode;
using DepthFunc = backend::SamplerCompareFunc;
using StencilCompareFunc = backend::SamplerCompareFunc;
using StencilOperation = backend::StencilOperation;
+5 -15
View File
@@ -2989,7 +2989,7 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::upscale(FrameGraph& fg, bool
}
if (dsrOptions.quality == QualityLevel::LOW) {
return upscaleBilinear(fg, translucent, dsrOptions, input, vp, outDesc, filter);
return upscaleBilinear(fg, dsrOptions, input, vp, outDesc, filter);
}
if (dsrOptions.quality == QualityLevel::MEDIUM) {
return upscaleSGSR1(fg, sourceHasLuminance, dsrOptions, input, vp, outDesc);
@@ -2997,7 +2997,7 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::upscale(FrameGraph& fg, bool
return upscaleFSR1(fg, dsrOptions, input, vp, outDesc);
}
FrameGraphId<FrameGraphTexture> PostProcessManager::upscaleBilinear(FrameGraph& fg, bool translucent,
FrameGraphId<FrameGraphTexture> PostProcessManager::upscaleBilinear(FrameGraph& fg,
DynamicResolutionOptions dsrOptions, FrameGraphId<FrameGraphTexture> const input,
filament::Viewport const& vp, FrameGraphTexture::Descriptor const& outDesc,
SamplerMagFilter filter) noexcept {
@@ -3016,18 +3016,10 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::upscaleBilinear(FrameGraph&
.attachments = { .color = { data.output } },
.clearFlags = TargetBufferFlags::DEPTH });
},
[this, vp, translucent, filter](FrameGraphResources const& resources,
[this, vp, filter](FrameGraphResources const& resources,
auto const& data, DriverApi& driver) {
bindPostProcessDescriptorSet(driver);
// helper to enable blending
auto enableTranslucentBlending = [](PipelineState& pipeline) {
pipeline.rasterState.blendFunctionSrcRGB = BlendFunction::ONE;
pipeline.rasterState.blendFunctionSrcAlpha = BlendFunction::ONE;
pipeline.rasterState.blendFunctionDstRGB = BlendFunction::ONE_MINUS_SRC_ALPHA;
pipeline.rasterState.blendFunctionDstAlpha = BlendFunction::ONE_MINUS_SRC_ALPHA;
};
auto color = resources.getTexture(data.input);
auto const& inputDesc = resources.getDescriptor(data.input);
@@ -3055,9 +3047,6 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::upscaleBilinear(FrameGraph&
auto out = resources.getRenderPassInfo();
auto pipeline = getPipelineState(material.getMaterial(mEngine));
if (translucent) {
enableTranslucentBlending(pipeline);
}
renderFullScreenQuad(out, pipeline, driver);
});
@@ -3065,7 +3054,8 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::upscaleBilinear(FrameGraph&
// if we had to take the low quality fallback, we still do the "sharpen pass"
if (dsrOptions.sharpness > 0.0f) {
output = rcas(fg, dsrOptions.sharpness, output, outDesc, translucent);
output = rcas(fg, dsrOptions.sharpness, output, outDesc,
false /* translucent=false, because dst buffer is allocated */);
}
// we rely on automatic culling of unused render passes
+1 -1
View File
@@ -257,7 +257,7 @@ public:
FrameGraphId<FrameGraphTexture> input, Viewport const& vp,
FrameGraphTexture::Descriptor const& outDesc, backend::SamplerMagFilter filter) noexcept;
FrameGraphId<FrameGraphTexture> upscaleBilinear(FrameGraph& fg, bool translucent,
FrameGraphId<FrameGraphTexture> upscaleBilinear(FrameGraph& fg,
DynamicResolutionOptions dsrOptions, FrameGraphId<FrameGraphTexture> input,
Viewport const& vp, FrameGraphTexture::Descriptor const& outDesc,
backend::SamplerMagFilter filter) noexcept;
+1 -4
View File
@@ -82,8 +82,6 @@ RenderPassBuilder& RenderPassBuilder::customCommand(
RenderPass RenderPassBuilder::build(FEngine const& engine, DriverApi& driver) const {
assert_invariant(mRenderableSoa);
assert_invariant(mScissorViewport.width <= std::numeric_limits<int32_t>::max());
assert_invariant(mScissorViewport.height <= std::numeric_limits<int32_t>::max());
return RenderPass{ engine, driver, *this };
}
@@ -107,8 +105,7 @@ void RenderPass::DescriptorSetHandleDeleter::operator()(
RenderPass::RenderPass(FEngine const& engine, DriverApi& driver,
RenderPassBuilder const& builder) noexcept
: mRenderableSoa(*builder.mRenderableSoa),
mColorPassDescriptorSet(builder.mColorPassDescriptorSet),
mScissorViewport(builder.mScissorViewport) {
mColorPassDescriptorSet(builder.mColorPassDescriptorSet) {
// compute the number of commands we need
updateSummedPrimitiveCounts(
+7 -9
View File
@@ -306,6 +306,12 @@ public:
// allocated commands ARE NOT freed, they're owned by the Arena
~RenderPass() noexcept;
// Specifies the viewport for the scissor rectangle, that is, the final scissor rect is
// offset by the viewport's left-top and clipped to the viewport's width/height.
void setScissorViewport(backend::Viewport const viewport) noexcept {
mScissorViewport = viewport;
}
Command const* begin() const noexcept { return mCommandBegin; }
Command const* end() const noexcept { return mCommandEnd; }
bool empty() const noexcept { return begin() == end(); }
@@ -461,7 +467,7 @@ private:
FScene::RenderableSoa const& mRenderableSoa;
ColorPassDescriptorSet const* const mColorPassDescriptorSet;
backend::Viewport const mScissorViewport{ 0, 0, INT32_MAX, INT32_MAX };
backend::Viewport mScissorViewport{ 0, 0, INT32_MAX, INT32_MAX };
Command const* /* const */ mCommandBegin = nullptr; // Pointer to the first command
Command const* /* const */ mCommandEnd = nullptr; // Pointer to one past the last command
mutable BufferObjectSharedHandle mInstancedUboHandle; // ubo for instanced primitives
@@ -476,7 +482,6 @@ class RenderPassBuilder {
RenderPass::Arena& mArena;
RenderPass::CommandTypeFlags mCommandTypeFlags{};
backend::Viewport mScissorViewport{ 0, 0, INT32_MAX, INT32_MAX };
FScene::RenderableSoa const* mRenderableSoa = nullptr;
utils::Range<uint32_t> mVisibleRenderables{};
math::float3 mCameraPosition{};
@@ -507,13 +512,6 @@ public:
return *this;
}
// Specifies the viewport for the scissor rectangle, that is, the final scissor rect is
// offset by the viewport's left-top and clipped to the viewport's width/height.
RenderPassBuilder& scissorViewport(backend::Viewport const viewport) noexcept {
mScissorViewport = viewport;
return *this;
}
// specifies the geometry to generate commands for
RenderPassBuilder& geometry(
FScene::RenderableSoa const& soa, utils::Range<uint32_t> const vr) noexcept {
+73 -67
View File
@@ -40,7 +40,6 @@
#include <utils/Panic.h>
#include <algorithm>
#include <optional>
#include <utility>
#include <stddef.h>
@@ -270,87 +269,94 @@ RendererUtils::ColorPassOutput RendererUtils::colorPass(
};
}
std::optional<RendererUtils::ColorPassOutput> RendererUtils::refractionPass(
RenderPass::Command const* RendererUtils::getFirstRefractionCommand(
RenderPass const& pass) noexcept {
// find the first refractive object in channel 2
RenderPass::Command const* const refraction = std::partition_point(pass.begin(), pass.end(),
[](auto const& command) {
constexpr uint64_t mask = RenderPass::CHANNEL_MASK | RenderPass::PASS_MASK;
constexpr uint64_t channel = uint64_t(RenderableManager::Builder::DEFAULT_CHANNEL) << RenderPass::CHANNEL_SHIFT;
constexpr uint64_t value = channel | uint64_t(RenderPass::Pass::REFRACT);
return (command.key & mask) < value;
});
const bool hasScreenSpaceRefraction =
(refraction->key & RenderPass::PASS_MASK) == uint64_t(RenderPass::Pass::REFRACT);
return hasScreenSpaceRefraction ? refraction : nullptr;
}
RendererUtils::ColorPassOutput RendererUtils::refractionPass(
FrameGraph& fg, FEngine& engine, FView const& view,
ColorPassInput colorPassInput,
ColorPassConfig config,
PostProcessManager::ScreenSpaceRefConfig const& ssrConfig,
PostProcessManager::ColorGradingConfig const colorGradingConfig,
RenderPass const& pass) noexcept {
RenderPass const& pass, RenderPass::Command const* const firstRefractionCommand) noexcept {
// find the first refractive object in channel 2
RenderPass::Command const* const refraction = std::partition_point(pass.begin(), pass.end(),
[](auto const& command) {
constexpr uint64_t mask = RenderPass::CHANNEL_MASK | RenderPass::PASS_MASK;
constexpr uint64_t channel = uint64_t(RenderableManager::Builder::DEFAULT_CHANNEL) << RenderPass::CHANNEL_SHIFT;
constexpr uint64_t value = channel | uint64_t(RenderPass::Pass::REFRACT);
return (command.key & mask) < value;
});
const bool hasScreenSpaceRefraction =
(refraction->key & RenderPass::PASS_MASK) == uint64_t(RenderPass::Pass::REFRACT);
assert_invariant(firstRefractionCommand);
RenderPass::Command const* const refraction = firstRefractionCommand;
// if there wasn't any refractive object, just skip everything below.
if (UTILS_UNLIKELY(hasScreenSpaceRefraction)) {
assert_invariant(!colorPassInput.linearColor);
assert_invariant(!colorPassInput.depth);
config.hasScreenSpaceReflectionsOrRefractions = true;
assert_invariant(!colorPassInput.linearColor);
assert_invariant(!colorPassInput.depth);
config.hasScreenSpaceReflectionsOrRefractions = true;
PostProcessManager& ppm = engine.getPostProcessManager();
auto const opaquePassOutput = colorPass(fg,
"Color Pass (opaque)", engine, view, colorPassInput, {
// When rendering the opaques, we need to conserve the sample buffer,
// so create a config that specifies the sample count.
.width = config.physicalViewport.width,
.height = config.physicalViewport.height,
.samples = config.msaa,
.format = config.hdrFormat
},
config, { .asSubpass = false, .customResolve = false },
pass.getExecutor(pass.begin(), refraction));
PostProcessManager& ppm = engine.getPostProcessManager();
auto const opaquePassOutput = colorPass(fg,
"Color Pass (opaque)", engine, view, colorPassInput, {
// When rendering the opaques, we need to conserve the sample buffer,
// so create a config that specifies the sample count.
.width = config.physicalViewport.width,
.height = config.physicalViewport.height,
.samples = config.msaa,
.format = config.hdrFormat
},
config, { .asSubpass = false, .customResolve = false },
pass.getExecutor(pass.begin(), refraction));
// Generate the mipmap chain
// Note: we can run some post-processing effects while the "color pass" descriptor set
// in bound because only the descriptor 0 (frame uniforms) matters, and it's
// present in both.
PostProcessManager::generateMipmapSSR(ppm, fg,
opaquePassOutput.linearColor,
ssrConfig.refraction,
true, ssrConfig);
// Generate the mipmap chain
// Note: we can run some post-processing effects while the "color pass" descriptor set
// in bound because only the descriptor 0 (frame uniforms) matters, and it's
// present in both.
PostProcessManager::generateMipmapSSR(ppm, fg,
opaquePassOutput.linearColor,
ssrConfig.refraction,
true, ssrConfig);
// Now we're doing the refraction pass proper.
// This uses the same framebuffer (color and depth) used by the opaque pass.
// For this reason, the `colorBufferDesc` parameter of colorPass() below is only used for
// the width and height.
colorPassInput.linearColor = opaquePassOutput.linearColor;
colorPassInput.depth = opaquePassOutput.depth;
// Now we're doing the refraction pass proper.
// This uses the same framebuffer (color and depth) used by the opaque pass.
// For this reason, the `colorBufferDesc` parameter of colorPass() below is only used for
// the width and height.
colorPassInput.linearColor = opaquePassOutput.linearColor;
colorPassInput.depth = opaquePassOutput.depth;
// Since we're reusing the existing target we don't want to clear any of its buffer.
// Important: if this target ended up being an imported target, then the clearFlags
// specified here wouldn't apply (the clearFlags of the imported target take precedence),
// and we'd end up clearing the opaque pass. This scenario never happens because it is
// prevented in Renderer.cpp's final blit.
config.clearFlags = TargetBufferFlags::NONE;
auto transparentPassOutput = colorPass(fg, "Color Pass (transparent)",
engine, view, colorPassInput, {
.width = config.physicalViewport.width,
.height = config.physicalViewport.height },
config, colorGradingConfig,
pass.getExecutor(refraction, pass.end()));
// Since we're reusing the existing target we don't want to clear any of its buffer.
// Important: if this target ended up being an imported target, then the clearFlags
// specified here wouldn't apply (the clearFlags of the imported target take precedence),
// and we'd end up clearing the opaque pass. This scenario never happens because it is
// prevented in Renderer.cpp's final blit.
config.clearFlags = TargetBufferFlags::NONE;
auto transparentPassOutput = colorPass(fg, "Color Pass (transparent)",
engine, view, colorPassInput, {
.width = config.physicalViewport.width,
.height = config.physicalViewport.height },
config, colorGradingConfig,
pass.getExecutor(refraction, pass.end()));
if (config.msaa > 1 && !colorGradingConfig.asSubpass) {
// We need to do a resolve here because later passes (such as color grading or DoF) will
// need to sample from 'output'. However, because we have MSAA, we know we're not
// sampleable. And this is because in the SSR case, we had to use a renderbuffer to
// conserve the multi-sample buffer.
transparentPassOutput.linearColor = ppm.resolve(fg, "Resolved Color Buffer",
transparentPassOutput.linearColor, { .levels = 1 });
}
return transparentPassOutput;
if (config.msaa > 1 && !colorGradingConfig.asSubpass) {
// We need to do a resolve here because later passes (such as color grading or DoF) will
// need to sample from 'output'. However, because we have MSAA, we know we're not
// sampleable. And this is because in the SSR case, we had to use a renderbuffer to
// conserve the multi-sample buffer.
transparentPassOutput.linearColor = ppm.resolve(fg, "Resolved Color Buffer",
transparentPassOutput.linearColor, { .levels = 1 });
}
return std::nullopt;
return transparentPassOutput;
}
UTILS_NOINLINE
+9 -6
View File
@@ -26,18 +26,19 @@
#include <filament/Viewport.h>
#include <backend/DriverEnums.h>
#include <backend/PixelBufferDescriptor.h>
#include <backend/Handle.h>
#include <math/vec2.h>
#include <math/vec4.h>
#include <stdint.h>
#include <optional>
#include <utility>
namespace filament {
namespace backend {
class PixelBufferDescriptor;
}
class FRenderTarget;
class FrameGraph;
class FrameGraph;
@@ -99,18 +100,20 @@ public:
PostProcessManager::ColorGradingConfig colorGradingConfig,
RenderPass::Executor passExecutor) noexcept;
static std::optional<ColorPassOutput> refractionPass(
static ColorPassOutput refractionPass(
FrameGraph& fg, FEngine& engine, FView const& view,
ColorPassInput colorPassInput,
ColorPassConfig config,
PostProcessManager::ScreenSpaceRefConfig const& ssrConfig,
PostProcessManager::ColorGradingConfig colorGradingConfig,
RenderPass const& pass) noexcept;
RenderPass const& pass, RenderPass::Command const* firstRefractionCommand) noexcept;
static void readPixels(backend::DriverApi& driver,
backend::Handle<backend::HwRenderTarget> renderTargetHandle,
uint32_t xoffset, uint32_t yoffset, uint32_t width, uint32_t height,
backend::PixelBufferDescriptor&& buffer);
static RenderPass::Command const* getFirstRefractionCommand(RenderPass const& pass) noexcept;
};
} // namespace filament
+19 -4
View File
@@ -98,6 +98,14 @@ namespace filament::fgviewer {
} // namespace filament::fgviewer
#endif
// We have added correctness assertions that breaks clients' projects. We add this define to allow
// for the client's to address these assertions at a more gradual pace.
#if defined(FILAMENT_RELAXED_CORRECTNESS_ASSERTIONS)
#define CORRECTNESS_ASSERTION_DEFAULT false
#else
#define CORRECTNESS_ASSERTION_DEFAULT true
#endif
namespace filament {
class Renderer;
@@ -715,10 +723,14 @@ public:
bool use_shadow_atlas = false;
} shadows;
struct {
// TODO: default the following two flags to true.
bool assert_material_instance_in_use = false;
bool assert_destroy_material_before_material_instance = false;
bool assert_vertex_buffer_count_exceeds_8 = false;
// TODO: clean-up the following flags (equivalent to setting them to true) when
// clients have addressed their usages.
bool assert_material_instance_in_use = CORRECTNESS_ASSERTION_DEFAULT;
bool assert_destroy_material_before_material_instance =
CORRECTNESS_ASSERTION_DEFAULT;
bool assert_vertex_buffer_count_exceeds_8 = CORRECTNESS_ASSERTION_DEFAULT;
bool assert_vertex_buffer_attribute_stride_mult_of_4 =
CORRECTNESS_ASSERTION_DEFAULT;
} debug;
} engine;
struct {
@@ -759,6 +771,9 @@ public:
{ "features.engine.debug.assert_vertex_buffer_count_exceeds_8",
"Assert when a client's number of buffers for a VertexBuffer exceeds 8.",
&features.engine.debug.assert_vertex_buffer_count_exceeds_8, false },
{ "features.engine.debug.assert_vertex_buffer_attribute_stride_mult_of_4",
"Assert that the attribute stride of a vertex buffer is a multiple of 4.",
&features.engine.debug.assert_vertex_buffer_attribute_stride_mult_of_4, false },
}};
utils::Slice<const FeatureFlag> getFeatureFlags() const noexcept {
+31 -18
View File
@@ -1125,16 +1125,6 @@ void FRenderer::renderJob(RootArenaScope& rootArenaScope, FView& view) {
// --------------------------------------------------------------------------------------------
// Color passes
// this makes the viewport relative to xvp
// FIXME: we should use 'vp' when rendering directly into the swapchain, but that's hard to
// know at this point. This will usually be the case when post-process is disabled.
// FIXME: we probably should take the dynamic scaling into account too
// if MSAA is enabled, we end-up rendering in an intermediate buffer. This is the only case where
// "!hasPostProcess" doesn't guarantee rendering into the swapchain.
const bool useIntermediateBuffer = hasPostProcess || msaaOptions.enabled ||
(isRenderingMultiview && engine.debug.stereo.combine_multiview_images);
passBuilder.scissorViewport(useIntermediateBuffer ? xvp : vp);
// This one doesn't need to be a FrameGraph pass because it always happens by construction
// (i.e. it won't be culled, unless everything is culled), so no need to complexify things.
passBuilder.variant(variant);
@@ -1174,8 +1164,35 @@ void FRenderer::renderJob(RootArenaScope& rootArenaScope, FView& view) {
passBuilder.renderFlags(renderFlags);
}
// create the pass, which generates all its commands (this is a heavy operation)
RenderPass const pass{ passBuilder.build(engine, driver) };
// now that we have the commands we can figure out if we have refraction commands
auto* const firstRefractionCommand = [&view](RenderPass const& pass) {
RenderPass::Command const* p = nullptr;
if (UTILS_UNLIKELY(view.isScreenSpaceRefractionEnabled() && !pass.empty())) {
p = RendererUtils::getFirstRefractionCommand(pass);
}
return p;
}(pass);
hasScreenSpaceRefraction = firstRefractionCommand != nullptr;
// this makes the viewport relative to xvp
// FIXME: we should use 'vp' when rendering directly into the swapchain, but that's hard to
// know at this point. This will usually be the case when post-process is disabled.
// FIXME: we probably should take the dynamic scaling into account too
// if MSAA is enabled, we end-up rendering in an intermediate buffer. This is the only case where
// "!hasPostProcess" doesn't guarantee rendering into the swapchain.
const bool useIntermediateBuffer = hasPostProcess || msaaOptions.enabled ||
ssReflectionsOptions.enabled || hasScreenSpaceRefraction ||
(isRenderingMultiview && engine.debug.stereo.
combine_multiview_images);
// this is slightly ugly, but conceptually `pass` is const; it's just that we can't set
// the scissor viewport during construction
const_cast<RenderPass&>(pass).setScissorViewport(useIntermediateBuffer ? xvp : vp);
FrameGraphTexture::Descriptor colorBufferDesc = {
.width = config.physicalViewport.width,
.height = config.physicalViewport.height,
@@ -1220,21 +1237,17 @@ void FRenderer::renderJob(RootArenaScope& rootArenaScope, FView& view) {
},
colorBufferDesc, config, colorGradingConfigForColor, pass.getExecutor());
if (view.isScreenSpaceRefractionEnabled() && !pass.empty()) {
if (UTILS_UNLIKELY(hasScreenSpaceRefraction)) {
// This cancels the colorPass() call above if refraction is active.
// The color pass + refraction + color-grading as subpass if needed
auto const output = RendererUtils::refractionPass(fg, mEngine, view, {
colorPassOutput = RendererUtils::refractionPass(fg, mEngine, view, {
.shadows = blackboard.get<FrameGraphTexture>("shadows"),
.ssao = blackboard.get<FrameGraphTexture>("ssao"),
.ssr = ssrConfig.ssr,
.structure = structure
},
config, ssrConfig, colorGradingConfigForColor, pass);
hasScreenSpaceRefraction = output.has_value();
if (hasScreenSpaceRefraction) {
colorPassOutput = output.value();
}
config, ssrConfig, colorGradingConfigForColor,
pass, firstRefractionCommand);
}
if (colorGradingConfig.customResolve) {
+34 -15
View File
@@ -386,8 +386,9 @@ void FTexture::setImage(FEngine& engine, size_t const level,
FILAMENT_CHECK_PRECONDITION(any(mUsage & Texture::Usage::UPLOADABLE))
<< "Texture is not uploadable.";
FILAMENT_CHECK_PRECONDITION(mSampleCount <= 1) << "Operation not supported with multisample ("
<< unsigned(mSampleCount) << ") texture.";
FILAMENT_CHECK_PRECONDITION(mSampleCount <= 1)
<< "Operation not supported with multisample ("
<< unsigned(mSampleCount) << ") texture.";
FILAMENT_CHECK_PRECONDITION(xoffset + width <= valueForLevel(level, mWidth))
<< "xoffset (" << unsigned(xoffset) << ") + width (" << unsigned(width)
@@ -428,28 +429,46 @@ void FTexture::setImage(FEngine& engine, size_t const level,
<< ") > texture depth (" << effectiveTextureDepthOrLayers << ") at level ("
<< unsigned(level) << ")";
if (UTILS_UNLIKELY(!width || !height || !depth)) {
// The operation is a no-op, return immediately. The PixelBufferDescriptor callback
// should be called automatically when the object is destroyed.
// The precondition check below assumes width, height, depth non null.
return;
}
if (p.type != PixelDataType::COMPRESSED) {
using PBD = PixelBufferDescriptor;
size_t const stride = p.stride ? p.stride : width;
size_t const bpp = PBD::computePixelSize(p.format, p.type);
size_t const bpp = PBD::computeDataSize(p.format, p.type, 1, 1, 1);
size_t const bpr = PBD::computeDataSize(p.format, p.type, stride, 1, p.alignment);
size_t const pbdWidth = stride;
// TODO: PBD should have a "layer stride" (using "depth" as a substitute).
size_t const pbdDepth = depth;
size_t const pbdHeight = p.size / bpr / pbdDepth;
assert_invariant(p.size % bpr == 0 && (p.size / bpr) % pbdDepth == 0);
size_t const bpl = bpr * height; // TODO: PBD should have a "layer stride"
// TODO: PBD should have a p.depth (# layers to skip)
FILAMENT_CHECK_PRECONDITION(
bpp * (pbdWidth - p.left) * (pbdHeight - p.top) * (pbdDepth - 0) >=
bpp * width * height * depth)
/* Calculates the byte offset of the last pixel in a 3D sub-region. */
auto const calculateLastPixelOffset = [bpp, bpr, bpl](
size_t xoff, size_t yoff, size_t zoff,
size_t width, size_t height, size_t depth) {
// The 0-indexed coordinates of the last pixel are:
// x = xoff + width - 1
// y = yoff + height - 1
// z = zoff + depth - 1
// The offset is calculated as: (z * bpl) + (y * bpr) + (x * bpp)
return ((zoff + depth - 1) * bpl) +
((yoff + height - 1) * bpr) +
((xoff + width - 1) * bpp);
};
size_t const lastPixelOffset = calculateLastPixelOffset(
p.left, p.top, 0, width, height, depth);
// make sure the whole last pixel is in the buffer
FILAMENT_CHECK_PRECONDITION(lastPixelOffset + bpp <= p.size)
<< "buffer overflow: (size=" << size_t(p.size) << ", stride=" << size_t(p.stride)
<< ", left=" << unsigned(p.left) << ", top=" << unsigned(p.top)
<< ") smaller than specified region "
"{{"
<< unsigned(xoffset) << "," << unsigned(yoffset) << "," << unsigned(zoffset)
<< "},{" << unsigned(width) << "," << unsigned(height) << "," << unsigned(depth)
<< ")}}";
<< unsigned(xoffset) << "," << unsigned(yoffset) << "," << unsigned(zoffset) << "},{"
<< unsigned(width) << "," << unsigned(height) << "," << unsigned(depth) << ")}}";
}
engine.getDriverApi().update3DImage(mHandle, uint8_t(level), xoffset, yoffset, zoffset, width,
+9 -3
View File
@@ -177,9 +177,15 @@ VertexBuffer* VertexBuffer::Builder::build(Engine& engine) {
<< "attribute " << j << " offset=" << attributes[j].offset
<< " is not multiple of 4";
FILAMENT_CHECK_PRECONDITION((attributes[j].stride & 0x3u) == 0)
<< "attribute " << j << " stride=" << attributes[j].stride
<< " is not multiple of 4";
FEngine* fengine = static_cast<FEngine*>(&engine);
if (fengine->features.engine.debug.assert_vertex_buffer_attribute_stride_mult_of_4) {
FILAMENT_CHECK_PRECONDITION((attributes[j].stride & 0x3u) == 0)
<< "attribute " << j << " stride=" << attributes[j].stride
<< " is not multiple of 4";
} else if ((attributes[j].stride & 0x3u) != 0) {
LOG(WARNING) << "attribute " << j << " stride=" << attributes[j].stride
<< " is not multiple of 4";
}
if (engine.getActiveFeatureLevel() == FeatureLevel::FEATURE_LEVEL_0) {
FILAMENT_CHECK_PRECONDITION(!(attributes[j].flags & Attribute::FLAG_INTEGER_TARGET))
+1
View File
@@ -16,6 +16,7 @@
#include "TangentsJob.h"
#include <cstdlib>
#include <memory>
#include <geometry/SurfaceOrientation.h>
+7 -6
View File
@@ -14,15 +14,12 @@ def _compare_goldens(base_dir, comparison_dir, out_dir=None):
for f in all_files)
all_results = []
for test_dir in test_dirs:
results_meta = {}
results = []
output_test_dir = None if not out_dir else os.path.join(out_dir, test_dir)
output_test_dir = None if not out_dir else os.path.abspath(os.path.join(out_dir, test_dir))
if output_test_dir:
mkdir_p(output_test_dir)
base_test_dir = os.path.abspath(os.path.join(base_dir, test_dir))
comp_test_dir = os.path.abspath(os.path.join(comparison_dir, test_dir))
results_meta['base_dir'] = base_test_dir
results_meta['comparison_dir'] = comp_test_dir
for golden_file in \
glob.glob(os.path.join(base_test_dir, "*.tif")):
base_fname = os.path.abspath(golden_file)
@@ -45,8 +42,12 @@ def _compare_goldens(base_dir, comparison_dir, out_dir=None):
result['result'] = RESULT_OK
results.append(result)
if output_test_dir:
results_meta['results'] = results
output_fname = os.path.join(output_test_dir, "compare_results.json")
results_meta = {
'results': results,
'base_dir': os.path.relpath(output_fname, base_test_dir),
'comparison_dir': os.path.relpath(output_fname, comp_test_dir),
}
with open(output_fname, 'w') as f:
f.write(json.dumps(results_meta, indent=2))
important_print(f'Written comparison results for {test_dir} to \n {output_fname}')
@@ -64,7 +65,7 @@ if __name__ == '__main__':
dest = args.dest
if not dest:
print('Assume the default renderdiff output folder')
dest = os.path.join(os.getcwd(), './out/renderdiff_tests')
dest = os.path.join(os.getcwd(), './out/renderdiff')
assert os.path.exists(dest), f"Destination folder={dest} does not exist."
results = _compare_goldens(args.src, dest, out_dir=args.out)
+182 -5
View File
@@ -17,6 +17,9 @@ import sys
import flask
import pathlib
import json
import requests
import io
import zipfile
from utils import ArgParseImpl
@@ -25,14 +28,163 @@ from flask import Flask, request, make_response, send_from_directory
DIR = pathlib.Path(__file__).parent.absolute()
HTML_DIR = os.path.join(DIR, "viewer_html")
# Generated by gemini
def _download_github_artifacts(pr_number, github_token, output_dir= ".") -> None:
"""
Downloads artifacts associated with a specific GitHub Pull Request.
This function performs the following steps:
1. Fetches the details of the Pull Request to get its head commit SHA.
2. Searches for GitHub Actions workflow runs triggered by that specific commit.
3. Iterates through successful workflow runs to find and list all associated artifacts.
4. Downloads each artifact (which comes as a ZIP file).
5. Extracts the contents of each downloaded ZIP file into a unique subdirectory
within the specified output directory.
Args:
owner (str): The GitHub repository owner (e.g., "octocat").
repo (str): The GitHub repository name (e.g., "Spoon-Knife").
pr_number (int): The Pull Request number.
output_dir (str): The local directory where downloaded artifacts will be saved.
Defaults to the current directory.
"""
# Prepare HTTP headers for GitHub API requests
headers = {"Accept": "application/vnd.github.v3+json"}
if github_token:
headers["Authorization"] = f"token {github_token}"
OWNER_REPO = 'google/filament'
# --- Step 1: Get PR details to find the head commit SHA ---
print(f"Fetching details for PR #{pr_number} in {OWNER_REPO}...")
pr_url = f"https://api.github.com/repos/{OWNER_REPO}/pulls/{pr_number}"
try:
response = requests.get(pr_url, headers=headers)
response.raise_for_status() # Raise an exception for HTTP errors (4xx or 5xx)
pr_data = response.json()
commit_sha = pr_data["head"]["sha"]
print(f"PR #{pr_number} is associated with commit SHA: {commit_sha}")
except requests.exceptions.HTTPError as e:
if e.response.status_code == 404:
print(f"Error: PR #{pr_number} not found in {OWNER_REPO}. Please check the PR number, owner, and repository name.")
elif e.response.status_code == 403:
print(f"Error: Access forbidden to PR #{pr_number}. You might be hitting API rate limits or need a valid GitHub Token.")
else:
print(f"An HTTP error occurred while fetching PR details: {e}")
return # Exit function on error
except requests.exceptions.RequestException as e:
print(f"A network error occurred while fetching PR details: {e}")
return # Exit function on error
# --- Step 2: Find workflow runs associated with the commit SHA ---
print(f"Searching for workflow runs for commit SHA: {commit_sha}...")
workflow_runs_url = f"https://api.github.com/repos/{OWNER_REPO}/actions/runs"
# Filter by head_sha and event='pull_request' for precision
params = {"head_sha": commit_sha, "event": "pull_request"}
try:
response = requests.get(workflow_runs_url, headers=headers, params=params)
response.raise_for_status()
runs_data = response.json()
workflow_runs = runs_data.get("workflow_runs", [])
if not workflow_runs:
print(f"No workflow runs found directly associated with PR #{pr_number} (commit SHA: {commit_sha}).")
print("This might happen if the workflow was triggered by a push after the PR was opened,")
print("or if the PR head branch was updated without triggering a new workflow run with this exact SHA.")
print("Consider checking GitHub Actions runs manually for this PR's branch on GitHub.")
return None
# Filter for runs that completed successfully
successful_runs = [run for run in workflow_runs if run.get("conclusion") == "success" and run.get("status") == "completed"]
if not successful_runs:
print(f"No *successful and completed* workflow runs found for PR #{pr_number} with commit SHA {commit_sha}. Exiting.")
return None
except requests.exceptions.HTTPError as e:
print(f"An HTTP error occurred while searching for workflow runs: {e}")
return None
except requests.exceptions.RequestException as e:
print(f"A network error occurred while searching for workflow runs: {e}")
return None
# Create the main output directory if it doesn't exist
os.makedirs(output_dir, exist_ok=True)
print(f"Ensuring output directory exists: {os.path.abspath(output_dir)}")
downloaded_any_artifact = False # Flag to track if any artifact was downloaded
# --- Step 3 & 4: List and Download Artifacts for each successful run ---
for run in successful_runs:
run_id = run["id"]
run_name = run["name"]
print(f"\nProcessing workflow run '{run_name}' (ID: {run_id})...")
artifacts_url = f"https://api.github.com/repos/{OWNER_REPO}/actions/runs/{run_id}/artifacts"
try:
response = requests.get(artifacts_url, headers=headers)
response.raise_for_status()
artifacts_data = response.json()
artifacts = artifacts_data.get("artifacts", [])
if not artifacts:
print(f" No artifacts found for workflow run ID {run_id}.")
continue # Move to the next workflow run
for artifact in artifacts:
artifact_id = artifact["id"]
artifact_name = artifact["name"]
archive_download_url = artifact["archive_download_url"]
print(f" Found artifact: '{artifact_name}' (ID: {artifact_id})")
# Perform the download request
print(f" Downloading '{artifact_name}'...")
# Use a copy of headers and specific Accept for ZIP download
download_headers = headers.copy()
download_headers["Accept"] = "application/vnd.github.v3+zip"
download_response = requests.get(archive_download_url, headers=download_headers, stream=True)
download_response.raise_for_status() # Check for errors in download
# --- Step 5: Extract the contents ---
# Use BytesIO to handle the zip file content in memory without saving to a temporary file
with io.BytesIO(download_response.content) as zip_buffer:
try:
with zipfile.ZipFile(zip_buffer, 'r') as zip_ref:
# Create a unique subdirectory for each artifact to avoid file name conflicts
extract_path = os.path.join(output_dir, f"{artifact_name}_{artifact_id}")
os.makedirs(extract_path, exist_ok=True)
zip_ref.extractall(extract_path)
print(f" Successfully extracted '{artifact_name}' to '{extract_path}/'")
downloaded_any_artifact = True
except zipfile.BadZipFile:
print(f" Error: Downloaded file for '{artifact_name}' is not a valid zip file. Skipping extraction.")
except Exception as e:
print(f" An error occurred during extraction of '{artifact_name}': {e}")
except requests.exceptions.HTTPError as e:
print(f" An HTTP error occurred while fetching artifacts for run {run_id}: {e}")
if e.response.status_code == 403:
print(" This often means you need a GitHub Personal Access Token with 'repo' scope (even for public repos for artifact downloads).")
continue # Continue processing the next workflow run
except requests.exceptions.RequestException as e:
print(f" A network error occurred while fetching artifacts for run {run_id}: {e}")
continue # Continue processing the next workflow run
if not downloaded_any_artifact:
print("\nNo artifacts were downloaded for the specified PR.")
else:
print("\nAll available artifacts have been processed.")
return 'Done'
def _create_app(config):
app = Flask(__name__)
client_config = config.copy()
base_dir = client_config['base_dir']
comparison_dir = client_config['comparison_dir']
diff_dir = client_config['diff_dir']
base_dir = os.path.join(diff_dir, client_config['base_dir'])
comparison_dir = os.path.join(diff_dir, client_config['comparison_dir'])
del client_config['base_dir']
del client_config['comparison_dir']
del client_config['diff_dir']
@@ -67,12 +219,37 @@ def _create_app(config):
if __name__ == '__main__':
PORT = 8901
parser = ArgParseImpl()
parser.add_argument('--diff', help='Diff directory', required=True)
parser.add_argument('--diff', type=str, help='Diff result directory')
parser.add_argument('--pr_number', type=str, help='Pull request artifacts to examine')
parser.add_argument('--github_token', type=str, help='Necessary for pull PR artifacts')
args, _ = parser.parse_known_args(sys.argv[1:])
with open(os.path.join(args.diff, 'compare_results.json'), 'r') as f:
if not args.diff and not args.pr_number:
print('Need to specify either a diff result directory or a Pull Request number')
exit(1)
if args.diff and args.pr_number:
print('Cannot specify both a diff result directory and a Pull Request number')
exit(1)
fdir = args.diff
if args.pr_number:
if not args.github_token:
print('Must provide --github_token to be able to download artifacts')
exit(1)
output_dir = f'/tmp/filament-pr{args.pr_number}-rdiff-result'
res = _download_github_artifacts(args.pr_number, args.github_token, output_dir)
if not res:
print('Failed to retrieve PR artifacts')
exit(1)
# TODO: Clean up the following so that we're not so specific on the paths diffs/presubmit
directory_name = list(os.listdir(output_dir))[0]
fdir = os.path.join(os.path.join(output_dir, directory_name), 'diffs/presubmit')
with open(os.path.join(fdir, 'compare_results.json'), 'r') as f:
config = json.loads(f.read())
config['diff_dir'] = os.path.abspath(args.diff)
config['diff_dir'] = os.path.abspath(fdir)
app = _create_app(config)
from waitress import serve