Compare commits

..

1 Commits

Author SHA1 Message Date
Benjamin Doherty
15424c85b5 Metal: use atomic for buffer tracking 2025-09-19 13:41:37 -07:00
32 changed files with 114 additions and 312 deletions

View File

@@ -31,7 +31,7 @@ repositories {
}
dependencies {
implementation 'com.google.android.filament:filament-android:1.65.2'
implementation 'com.google.android.filament:filament-android:1.65.0'
}
```
@@ -51,7 +51,7 @@ Here are all the libraries available in the group `com.google.android.filament`:
iOS projects can use CocoaPods to install the latest release:
```shell
pod 'Filament', '~> 1.65.2'
pod 'Filament', '~> 1.65.0'
```
## Documentation

View File

@@ -7,9 +7,6 @@ A new header is inserted each time a *tag* is created.
Instead, if you are authoring a PR for the main branch, add your release note to
[NEW_RELEASE_NOTES.md](./NEW_RELEASE_NOTES.md).
## v1.65.2
## v1.65.1
- `setFrameScheduledCallback` now works on all backends (frame presentation scheduling is still only

View File

@@ -1,5 +1,5 @@
GROUP=com.google.android.filament
VERSION_NAME=1.65.2
VERSION_NAME=1.65.0
POM_DESCRIPTION=Real-time physically based rendering engine for Android.

View File

@@ -78,11 +78,5 @@
},
"docs_src/README.md": {
"dest": "dup/docs.md"
},
"test/renderdiff/README.md": {
"dest": "dup/renderdiff.md",
"link_transforms": {
"docs/images/": "../images/"
}
}
}

View File

@@ -27,8 +27,6 @@
- [Code coverage analysis](./notes/coverage.md)
- [Performance analysis](./notes/performance_analysis.md)
- [Framegraph](./notes/framegraph.md)
- [Tests](./notes/tests.md)
- [renderdiff](./dup/renderdiff.md)
- [Libraries](./notes/libs.md)
- [bluegl](./dup/bluegl.md)
- [bluevk](./dup/bluevk.md)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 KiB

View File

@@ -1,4 +0,0 @@
# Tests
Filament has a collection of tests that can be run locally. Many of them are used
for in our Continuation Integration flow [on github](https://https://github.com/google/filament/tree/main/.github/workflows).

View File

@@ -63,7 +63,6 @@ public:
static void copyImage(uint8_t* UTILS_RESTRICT dest,
const uint8_t* UTILS_RESTRICT src,
size_t srcBytesPerRow, size_t /*srcChannelCount*/,
size_t /*dstRowOffset */, size_t /*dstColumnOffset */,
size_t dstBytesPerRow, size_t /*dstChannelCount*/,
size_t /*width*/, size_t height, bool /*swizzle*/) {
if (srcBytesPerRow == dstBytesPerRow) {
@@ -80,20 +79,17 @@ public:
template<typename dstComponentType, typename srcComponentType>
static void reshapeImage(uint8_t* UTILS_RESTRICT dest, const uint8_t* UTILS_RESTRICT src,
size_t srcBytesPerRow,
size_t srcChannelCount,
size_t dstRowOffset, size_t dstColumnOffset,
size_t dstBytesPerRow, size_t dstChannelCount,
size_t srcChannelCount, size_t dstBytesPerRow, size_t dstChannelCount,
size_t width, size_t height, bool swizzle) {
const dstComponentType dstMaxValue = getMaxValue<dstComponentType>();
const srcComponentType srcMaxValue = getMaxValue<srcComponentType>();
const size_t minChannelCount = math::min(srcChannelCount, dstChannelCount);
assert_invariant(minChannelCount <= 4);
UTILS_ASSUME(minChannelCount <= 4);
dest += (dstRowOffset * dstBytesPerRow);
const int inds[4] = { swizzle ? 2 : 0, 1, swizzle ? 0 : 2, 3 };
for (size_t row = 0; row < height; ++row) {
const srcComponentType* in = (const srcComponentType*) src;
dstComponentType* out = (dstComponentType*)dest + (dstColumnOffset * dstChannelCount);
const srcComponentType* in = (const srcComponentType*)src;
dstComponentType* out = (dstComponentType*)dest;
for (size_t column = 0; column < width; ++column) {
for (size_t channel = 0; channel < minChannelCount; ++channel) {
if constexpr (std::is_same_v<dstComponentType, srcComponentType>) {
@@ -132,23 +128,17 @@ public:
default: return false;
}
void (*reshaper)(uint8_t* dest, const uint8_t* src, size_t srcBytesPerRow,
size_t srcChannelCount,
size_t srcRowOffset, size_t srcColumnOffset,
size_t dstBytesPerRow, size_t dstChannelCount,
size_t srcChannelCount, size_t dstBytesPerRow, size_t dstChannelCount,
size_t width, size_t height, bool swizzle) = nullptr;
constexpr auto UBYTE = PixelDataType::UBYTE;
constexpr auto FLOAT = PixelDataType::FLOAT;
constexpr auto UINT = PixelDataType::UINT;
constexpr auto INT = PixelDataType::INT;
constexpr auto HALF = PixelDataType::HALF;
constexpr auto UBYTE = PixelDataType::UBYTE, FLOAT = PixelDataType::FLOAT,
UINT = PixelDataType::UINT, INT = PixelDataType::INT;
switch (dst->type) {
case UBYTE:
switch (srcType) {
case UBYTE:
reshaper = reshapeImage<uint8_t, uint8_t>;
if (dst->format == PixelDataFormat::RGBA &&
dstChannelCount == srcChannelCount && !swizzle && dst->top == 0 &&
dst->left == 0) {
dstChannelCount == srcChannelCount && !swizzle) {
reshaper = copyImage;
}
break;
@@ -185,20 +175,13 @@ public:
default: return false;
}
break;
case HALF:
switch (srcType) {
case HALF: reshaper = copyImage; break;
default: return false;
}
break;
default:
return false;
}
uint8_t* dstBytes = (uint8_t*) dst->buffer;
const int dstBytesPerRow = PixelBufferDescriptor::computeDataSize(dst->format, dst->type,
dst->stride ? dst->stride : width, 1, dst->alignment);
reshaper(dstBytes, srcBytes, srcBytesPerRow, srcChannelCount,
dst->top, dst->left, dstBytesPerRow,
reshaper(dstBytes, srcBytes, srcBytesPerRow, srcChannelCount, dstBytesPerRow,
dstChannelCount, width, height, swizzle);
return true;
}

View File

@@ -153,7 +153,7 @@ private:
Type mType = Type::NONE;
static PlatformMetal* platform;
static std::array<uint64_t, TypeCount> aliveBuffers;
static std::array<std::atomic<uint64_t>, TypeCount> aliveBuffers;
};
class MetalBuffer {

View File

@@ -24,7 +24,7 @@
namespace filament {
namespace backend {
std::array<uint64_t, TrackedMetalBuffer::TypeCount> TrackedMetalBuffer::aliveBuffers = { 0 };
std::array<std::atomic<uint64_t>, TrackedMetalBuffer::TypeCount> TrackedMetalBuffer::aliveBuffers = { 0 };
PlatformMetal* TrackedMetalBuffer::platform = nullptr;
PlatformMetal* ScopedAllocationTimer::platform = nullptr;

View File

@@ -102,7 +102,7 @@ id<MTLCommandBuffer> getPendingCommandBuffer(MetalContext* context) {
context->pendingCommandBuffer = [context->commandQueue commandBuffer];
// It's safe for this block to capture the context variable. MetalDriver::terminate will ensure
// all frames and their completion handlers finish before context is deallocated.
const uint64_t thisCommandBufferId = context->pendingCommandBufferId;
uint64_t thisCommandBufferId = context->pendingCommandBufferId;
[context->pendingCommandBuffer addCompletedHandler:^(id <MTLCommandBuffer> buffer) {
context->resourceTracker.clearResources((__bridge void*) buffer);
@@ -118,11 +118,6 @@ id<MTLCommandBuffer> getPendingCommandBuffer(MetalContext* context) {
context->memorylessLimitsReached = true;
}
}
if (UTILS_UNLIKELY(errorCode != MTLCommandBufferErrorNone)) {
LOG(ERROR) << "Filament Metal command buffer errored with code: " << errorCode << " ("
<< stringifyMTLCommandBufferError(errorCode) << ").";
}
}];
FILAMENT_CHECK_POSTCONDITION(context->pendingCommandBuffer)
<< "Could not obtain command buffer.";

View File

@@ -451,38 +451,6 @@ inline MTLTextureSwizzleChannels getSwizzleChannels(TextureSwizzle r, TextureSwi
getSwizzle(a));
}
inline const char* stringifyMTLCommandBufferError(MTLCommandBufferError error) {
#if !defined(FILAMENT_IOS)
if (error == MTLCommandBufferErrorDeviceRemoved) {
return "MTLCommandBufferErrorDeviceRemoved";
}
#endif
switch (error) {
case MTLCommandBufferErrorNone:
return "MTLCommandBufferErrorNone";
case MTLCommandBufferErrorInternal:
return "MTLCommandBufferErrorInternal";
case MTLCommandBufferErrorTimeout:
return "MTLCommandBufferErrorTimeout";
case MTLCommandBufferErrorPageFault:
return "MTLCommandBufferErrorPageFault";
case MTLCommandBufferErrorAccessRevoked:
return "MTLCommandBufferErrorAccessRevoked";
case MTLCommandBufferErrorNotPermitted:
return "MTLCommandBufferErrorNotPermitted";
case MTLCommandBufferErrorOutOfMemory:
return "MTLCommandBufferErrorOutOfMemory";
case MTLCommandBufferErrorInvalidResource:
return "MTLCommandBufferErrorInvalidResource";
case MTLCommandBufferErrorMemoryless:
return "MTLCommandBufferErrorMemoryless";
case MTLCommandBufferErrorStackOverflow:
return "MTLCommandBufferErrorStackOverflow";
default:
return "Unknown";
}
}
} // namespace backend
} // namespace filament

View File

@@ -107,11 +107,8 @@ VulkanCommandBuffer::VulkanCommandBuffer(VulkanContext const& context, VkDevice
.sType = VK_STRUCTURE_TYPE_EXPORT_FENCE_CREATE_INFO,
.handleTypes = context.getFenceExportFlags()
};
fenceCreateInfo.pNext = &exportFenceCreateInfo;
// Necessary to guard this. Otherwise, swiftshader would throw an error.
if (context.getFenceExportFlags()) {
fenceCreateInfo.pNext = &exportFenceCreateInfo;
}
vkCreateFence(device, &fenceCreateInfo, VKALLOC, &mFence);
}

View File

@@ -269,10 +269,6 @@ TEST_F(BlitTest, ColorResolve) {
SKIP_IF(Backend::WEBGPU, "test cases fail in WebGPU, see b/424157731");
auto& api = getDriverApi();
if (api.getFeatureLevel() < FeatureLevel::FEATURE_LEVEL_2) {
GTEST_SKIP() << "Skipping test because multi-sampled textures are not supported at Feature level < 2";
}
constexpr int kSrcTexWidth = 256;
constexpr int kSrcTexHeight = 256;
constexpr int kDstTexWidth = 256;

View File

@@ -392,6 +392,10 @@ TEST_F(LoadImageTest, UpdateImage2D) {
}
TEST_F(LoadImageTest, UpdateImageSRGB) {
FAIL_IF(SkipEnvironment(OperatingSystem::APPLE, Backend::VULKAN),
"Crashing when reading pixels without a redundant call to makeCurrent right before the"
"render pass. b/422798473");
auto& api = getDriverApi();
Cleanup cleanup(api);
api.startCapture();
@@ -476,6 +480,10 @@ TEST_F(LoadImageTest, UpdateImageSRGB) {
}
TEST_F(LoadImageTest, UpdateImageMipLevel) {
FAIL_IF(SkipEnvironment(OperatingSystem::APPLE, Backend::VULKAN),
"Crashing when reading pixels without a redundant call to makeCurrent right before the"
"render pass. b/422798473");
auto& api = getDriverApi();
Cleanup cleanup(api);
api.startCapture();
@@ -511,6 +519,8 @@ TEST_F(LoadImageTest, UpdateImageMipLevel) {
PixelBufferDescriptor descriptor = checkerboardPixelBuffer(pixelFormat, pixelType, kTexSize);
api.update3DImage(texture, /* level*/ 1, 0, 0, 0, kTexSize, kTexSize, 1, std::move(descriptor));
api.beginFrame(0, 0, 0);
// Update samplers.
DescriptorSetHandle descriptorSet = shader.createDescriptorSet(api);
api.updateDescriptorSetTexture(descriptorSet, 0, texture, {
@@ -534,12 +544,13 @@ TEST_F(LoadImageTest, UpdateImageMipLevel) {
api.bindRenderPrimitive(mTriangle.getRenderPrimitive());
api.draw2(0, 3, 1);
api.endRenderPass();
EXPECT_IMAGE(defaultRenderTarget,
ScreenshotParams(kTexSize, kTexSize, "UpdateImageMipLevel", 1875922935));
}
EXPECT_IMAGE(defaultRenderTarget,
ScreenshotParams(kTexSize, kTexSize, "UpdateImageMipLevel", 1875922935));
api.commit(swapChain);
api.endFrame(0);
api.stopCapture();
}

View File

@@ -23,7 +23,6 @@
#include "Skip.h"
#include "TrianglePrimitive.h"
#include <utils/debug.h>
#include <utils/Hash.h>
#include <fstream>
@@ -82,6 +81,8 @@ public:
TEST_F(ReadPixelsTest, ReadPixels) {
SKIP_IF(Backend::WEBGPU, "test cases fail in WebGPU, see b/424157731");
NONFATAL_FAIL_IF(SkipEnvironment(OperatingSystem::APPLE, Backend::VULKAN),
"Two cases fail, see b/417255941 and b/417255943");
// These test scenarios use a known hash of the result pixel buffer to decide pass / fail,
// asserting an exact pixel-for-pixel match. So far, rendering on macOS and iPhone have had
// deterministic results. Take this test with a grain of salt, however, as other platform / GPU
@@ -123,15 +124,11 @@ TEST_F(ReadPixelsTest, ReadPixels) {
size_t bufferDimension = getRenderTargetSize();
size_t getBufferSizeBytes() const {
return bufferDimension * bufferDimension * getBytesPerPixels();
}
size_t getBytesPerPixels() const {
int bpp;
size_t components;
int bpp;
getPixelInfo(format, type, components, bpp);
return bpp;
};
return bufferDimension * bufferDimension * bpp;
}
// The offset and stride set on the pixel buffer.
size_t left = 0, top = 0, alignment = 1;
@@ -148,21 +145,7 @@ TEST_F(ReadPixelsTest, ReadPixels) {
const size_t width = readRect.width, height = readRect.height;
LinearImage image(width, height, 4);
if (format == PixelDataFormat::RGBA && type == PixelDataType::UBYTE) {
if (top == 0 && left == 0 && renderTargetBaseSize == readRect.width) {
image = toLinearWithAlpha<uint8_t>(width, height, width * 4, (uint8_t*)pixelData);
} else {
float* pixelRef = image.getPixelRef();
size_t const bpp = getBytesPerPixels();
size_t const bpr = bpp * bufferDimension;
for (size_t l = left; l < left + width; ++l) {
for (size_t t = top; t < top + height; ++t) {
for (size_t c = 0; c < 4; ++c) {
pixelRef[c + (l - left) * 4 + ((t - top) * 4 * width)] =
((uint8_t*)pixelData)[c + l * bpp + t * bpr] / 255.0f;
}
}
}
}
image = toLinearWithAlpha<uint8_t>(width, height, width * 4, (uint8_t*)pixelData);
}
if (format == PixelDataFormat::RGBA && type == PixelDataType::FLOAT) {
memcpy(image.getPixelRef(), pixelData, width * height * sizeof(math::float4));
@@ -186,17 +169,14 @@ TEST_F(ReadPixelsTest, ReadPixels) {
// The normative read pixels test case. Render a white triangle over a blue background and read
// the full viewport into a pixel buffer.
TestCase const t0(renderTargetBaseSize);
TestCase::Rect const subregionRect = {
.x = 90,
.y = 403,
.width = 64,
.height = 64,
};
// Check that a subregion of the render target can be read into a pixel buffer.
TestCase t2(renderTargetBaseSize);
t2.testName = "readPixels_subregion";
t2.readRect = subregionRect;
t2.readRect.x = 90;
t2.readRect.y = 403;
t2.readRect.width = 64;
t2.readRect.height = 64;
t2.bufferDimension = 64;
t2.hash = 0xcba7675a;
@@ -219,17 +199,16 @@ TEST_F(ReadPixelsTest, ReadPixels) {
// Check that readPixels can read a region of the render target into a subregion of a large
// buffer.
const size_t actualBufferSize = renderTargetBaseSize * 2;
constexpr size_t readOffset = 64;;
assert_invariant(renderTargetBaseSize + readOffset < actualBufferSize);
TestCase t5(renderTargetBaseSize);
t5.testName = "readPixels_subbuffer";
t5.readRect = subregionRect;
t5.bufferDimension = actualBufferSize;
t5.left = readOffset;
t5.top = readOffset;
t5.hash = 0x64585D10;
t5.readRect.x = 90;
t5.readRect.y = 403;
t5.readRect.width = 64;
t5.readRect.height = 64;
t5.bufferDimension = 512;
t5.left = 64;
t5.top = 64;
t5.hash = 0xbaefdb54;
// Check that readPixels works with integer formats.
TestCase t6(renderTargetBaseSize);
@@ -374,6 +353,8 @@ TEST_F(ReadPixelsTest, ReadPixels) {
api.commit(swapChain);
api.endFrame(0);
}
// This ensures all driver commands have finished before exiting the test.
flushAndWait();
}

View File

@@ -1,12 +1,12 @@
Pod::Spec.new do |spec|
spec.name = "Filament"
spec.version = "1.65.2"
spec.version = "1.65.0"
spec.license = { :type => "Apache 2.0", :file => "LICENSE" }
spec.homepage = "https://google.github.io/filament"
spec.authors = "Google LLC."
spec.summary = "Filament is a real-time physically based rendering engine for Android, iOS, Windows, Linux, macOS, and WASM/WebGL."
spec.platform = :ios, "11.0"
spec.source = { :http => "https://github.com/google/filament/releases/download/v1.65.2/filament-v1.65.2-ios.tgz" }
spec.source = { :http => "https://github.com/google/filament/releases/download/v1.65.0/filament-v1.65.0-ios.tgz" }
# Fix linking error with Xcode 12; we do not yet support the simulator on Apple silicon.
spec.pod_target_xcconfig = {

View File

@@ -17,13 +17,11 @@
#ifndef TNT_FILABRIDGE_UIBSTRUCTS_H
#define TNT_FILABRIDGE_UIBSTRUCTS_H
#include <math/mat3.h>
#include <math/mat4.h>
#include <math/vec4.h>
#include <private/filament/EngineEnums.h>
#include <array>
#include <string_view>
/*
@@ -40,8 +38,7 @@ struct alignas(16) vec3 : public std::array<float, 3> {};
struct alignas(16) vec4 : public std::array<float, 4> {};
struct mat33 : public std::array<vec3, 3> {
// passing by value informs the compiler that rhs != *this
mat33& operator=(math::mat3f const rhs) noexcept {
mat33& operator=(math::mat3f const& rhs) noexcept {
for (int i = 0; i < 3; i++) {
(*this)[i][0] = rhs[i][0];
(*this)[i][1] = rhs[i][1];
@@ -52,8 +49,7 @@ struct mat33 : public std::array<vec3, 3> {
};
struct mat44 : public std::array<vec4, 4> {
// passing by value informs the compiler that rhs != *this
mat44& operator=(math::mat4f const rhs) noexcept {
mat44& operator=(math::mat4f const& rhs) noexcept {
for (int i = 0; i < 4; i++) {
(*this)[i][0] = rhs[i][0];
(*this)[i][1] = rhs[i][1];

View File

@@ -146,7 +146,7 @@ protected:
Platform mPlatform = Platform::DESKTOP;
TargetApi mTargetApi = (TargetApi) 0;
Optimization mOptimization = Optimization::PERFORMANCE;
Workarounds mWorkarounds = Workarounds::ALL;
Workarounds mWorkarounds = Workarounds::NONE;
bool mPrintShaders = false;
bool mSaveRawVariants = false;
bool mGenerateDebugInfo = false;
@@ -805,16 +805,17 @@ public:
// Returns true if any of the parameter samplers matches the specified type.
bool hasSamplerType(SamplerType samplerType) const noexcept;
static constexpr size_t MAX_PARAMETERS_COUNT = 48;
static constexpr size_t MAX_SUBPASS_COUNT = 1;
static constexpr size_t MAX_BUFFERS_COUNT = 4;
using ParameterList = std::vector<Parameter>;
using ParameterList = Parameter[MAX_PARAMETERS_COUNT];
using SubpassList = Parameter[MAX_SUBPASS_COUNT];
using BufferList = std::vector<std::unique_ptr<filament::BufferInterfaceBlock>>;
using ConstantList = std::vector<Constant>;
using PushConstantList = std::vector<PushConstant>;
// returns the number of parameters declared in this material
size_t getParameterCount() const noexcept { return mParameters.size(); }
uint8_t getParameterCount() const noexcept { return mParameterCount; }
// returns a list of at least getParameterCount() parameters
const ParameterList& getParameters() const noexcept { return mParameters; }
@@ -960,6 +961,7 @@ private:
bool mShadowMultiplier = false;
bool mTransparentShadow = false;
uint8_t mParameterCount = 0;
uint8_t mSubpassCount = 0;
bool mDoubleSided = false;

View File

@@ -295,7 +295,8 @@ MaterialBuilder& MaterialBuilder::variable(Variable v,
MaterialBuilder& MaterialBuilder::parameter(const char* name, size_t size, UniformType type,
ParameterPrecision precision) {
mParameters.emplace_back(name, type, size, precision );
FILAMENT_CHECK_POSTCONDITION(mParameterCount < MAX_PARAMETERS_COUNT) << "Too many parameters";
mParameters[mParameterCount++] = { name, type, size, precision };
return *this;
}
@@ -314,8 +315,9 @@ MaterialBuilder& MaterialBuilder::parameter(const char* name, SamplerType sample
<< "multisample samplers only possible with SAMPLER_2D or SAMPLER_2D_ARRAY,"
" as long as type is not SHADOW";
mParameters.emplace_back( name, samplerType, format, precision, filterable,
multisample, transformName, stages );
FILAMENT_CHECK_POSTCONDITION(mParameterCount < MAX_PARAMETERS_COUNT) << "Too many parameters";
mParameters[mParameterCount++] = { name, samplerType, format, precision, filterable,
multisample, transformName, stages };
return *this;
}
@@ -639,7 +641,7 @@ MaterialBuilder& MaterialBuilder::shaderDefine(const char* name, const char* val
}
bool MaterialBuilder::hasSamplerType(SamplerType const samplerType) const noexcept {
for (size_t i = 0, c = mParameters.size(); i < c; i++) {
for (size_t i = 0, c = mParameterCount; i < c; i++) {
auto const& param = mParameters[i];
if (param.isSampler() && param.samplerType == samplerType) {
return true;
@@ -666,7 +668,7 @@ void MaterialBuilder::prepareToBuild(MaterialInfo& info) noexcept {
BufferInterfaceBlock::Builder ibb;
// sampler bindings start at 1, 0 is the ubo
uint16_t binding = 1;
for (size_t i = 0, c = mParameters.size(); i < c; i++) {
for (size_t i = 0, c = mParameterCount; i < c; i++) {
auto const& param = mParameters[i];
assert_invariant(!param.isSubpass());
if (param.isSampler()) {

View File

@@ -171,7 +171,7 @@ protected:
StringReplacementMap mTemplateMap;
StringReplacementMap mMaterialParameters;
filament::UserVariantFilterMask mVariantFilter = 0;
Workarounds mWorkarounds = Workarounds::ALL;
Workarounds mWorkarounds = Workarounds::NONE;
bool mIncludeEssl1 = true;
};

View File

@@ -118,12 +118,12 @@ bool MaterialParser::ignoreLexeme(const MaterialLexeme&, MaterialBuilder&) const
}
static bool reflectParameters(const MaterialBuilder& builder) {
size_t const count = builder.getParameterCount();
uint8_t const count = builder.getParameterCount();
const MaterialBuilder::ParameterList& parameters = builder.getParameters();
std::cout << "{" << std::endl;
std::cout << " \"parameters\": [" << std::endl;
for (size_t i = 0; i < count; i++) {
for (uint8_t i = 0; i < count; i++) {
const MaterialBuilder::Parameter& parameter = parameters[i];
std::cout << " {" << std::endl;
std::cout << R"( "name": ")" << parameter.name.c_str() << "\"," << std::endl;

View File

@@ -479,7 +479,7 @@ constexpr TQuaternion<T> TMat33<T>::packTangentFrame(const TMat33<T>& m, size_t
template<typename T>
constexpr details::TMat33<T> prescaleForNormals(const details::TMat33<T>& m) noexcept {
return m * details::TMat33<T>(
T(1.0) / std::sqrt(max(float3{length2(m[0]), length2(m[1]), length2(m[2])})));
1.0 / std::sqrt(max(float3{length2(m[0]), length2(m[1]), length2(m[2])})));
}
// ----------------------------------------------------------------------------------------

View File

@@ -13,33 +13,11 @@ description file) and then running gltf_viewer to produce the renderings.
In the `test` directory is a list of test descriptions that are specified in json. Please see
`sample.json` to parse the structure.
## Setting up python
The `renderdiff` project uses `python` extensively. To install the dependencies for producing
renderings, do the following step
- Set up a virtual environment (from the root directory)
```
python3 -m venv venv
. ./venv/bin/activate
```
- Install the rendering dependencies
```
pip install -r test/renderdiff/src/rendering_requirements.txt
```
- Install the viewer depdencies
```
pip install -r test/renderdiff/src/viewer_requirements.txt
```
- For the commands in the following section, do not exit the virtual environment. Once you've
completed all your work, you can exit with
```
deactivate
```
## Running the test locally
- To run the same presbumit as [`test-renderdiff`](presubmit-renderdiff), you can do
```
bash test/renderdiff/local_test.sh
bash test/renderdiff/test.sh
```
- This script will generate the renderings based on the current state of your repo.
@@ -89,7 +67,7 @@ in the following fashion
- Copy the new images to their appropriate place in `filament-assets`
- Push the `filament-assets` working branch to remote
```
git push origin my-pr-branch-golden
```
@@ -100,39 +78,14 @@ in the following fashion
RDIFF_BBRANCH=my-pr-branch-golden
```
### Manually updating the golden repo
Doing the above has multiple effects:
- The presubmit test [`test-renderdiff`][presubmit-renderdiff] will test against the provided
branch of the golden repo (i.e. `my-pr-branch-golden`).
- If the PR is merged, then there is another workflow that will merge `my-pr-branch-golden` to
the `main` branch of the golden repo.
## Viewing test results
We provide a viewer for looking at the result of a test run. The viewer is a webapp that can be used by
pointing your browser to a localhost port. If you input the viewer with a PR or a directory, it will
parse the test result and show the results and the rendered and/or golden images.
![Viewer](docs/images/renderdiff_example.png)
To run the viewer of a test output directory that has been generated locally, you would run the
following
```
python3 test/renderdiff/src/viewer.py --diff=[test output]
```
where `[test output]` is a directory containing the `compare_results.json` of the test run.
For example, it could be `out/renderdiff/diffs/presubmit` for the standard path to the
`presubmit` test output.
To see the results of a Pull Request initiated test run, you would do the following
```
python3 test/renderdiff/src/viewer.py --pr_number=[PR #] --github_token=[github token]
```
where `[PR #]` is the numeric ID of your pull request, and the `[github token]` is an acess token
that you (as a github user) needs to generate ([reference][github_token_ref]).
[github_token_ref]: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens
[Mesa]: https://docs.mesa3d.org
[SwiftShader]: https://github.com/google/swiftshader
[presubmit-renderdiff]: https://github.com/google/filament/blob/e85dfe75c86106a05019e13ccdbef67e030af675/.github/workflows/presubmit.yml#L118

View File

@@ -22,6 +22,7 @@ import concurrent.futures
from utils import execute, ArgParseImpl, mkdir_p, mv_f, important_print
import test_config
from golden_manager import GoldenManager
from image_diff import same_image
from results import RESULT_OK, RESULT_FAILED

View File

@@ -1,6 +0,0 @@
Mako==1.3.10
MarkupSafe==3.0.2
numpy==2.3.3
PyYAML==6.0.2
setuptools==80.9.0
tifffile==2025.9.9

View File

@@ -76,12 +76,9 @@ class TestConfig(RenderingConfig):
given_presets = {p.name: p for p in presets}
assert all((name in given_presets) for name in apply_presets),\
f'used preset {name} which is not in {given_presets}'
# Note that this needs to applied in order. Models will be overwritten.
# Properties will be "added" in order.
for preset in apply_presets:
rendering.update(given_presets[preset].rendering)
preset_models = given_presets[preset].models
preset_models += given_presets[preset].models
assert 'rendering' in data
rendering.update(data['rendering'])

View File

@@ -113,7 +113,7 @@ class ExpandedComparisonResult extends LitElement {
#diffCanvas {
width: 100%;
height: 100%;
margin-top: 22px;
margin-top: 35px;
margin-bottom: 5px;
}
.selector {
@@ -151,9 +151,33 @@ class ExpandedComparisonResult extends LitElement {
_viewer(name, choices, current, viewType) {
const url = viewType == 'rendered' ? getCompUrl(current) : getGoldenUrl(current);
const onSelect = (ev) => {
const testName = ev.target.value;
const test = this.tests.find((t) => t.name == testName);
if (name == 'left') {
this.left = test;
this.leftImageLoaded = false;
} else {
this.right = test;
this.rightImageLoaded = false;
}
this.showDiff = false;
};
const dropdown = () => {
if (this.disableDropdowns) {
return html`<div class="selector">${current.name} (${viewType})</div>`;
}
return html`
<select class="selector" @change=${onSelect}>
${choices.map((c) => html`<option value=${c.name} ?selected=${c.name == current.name}>${c.name}</option>`)}
</select>
`;
}
return html`
<div style="flex: 1; margin: 0 5px;">
<div>${current.name}</div>
${dropdown()}
<tiff-viewer id="viewer-${name}" class="viewer"
name="${current.name}"
fileurl="${url}"></tiff-viewer>
@@ -381,9 +405,6 @@ class App extends LitElement {
selectedTests: {type: Array},
comparisonContent: {type: Object},
compareMode: {type: Boolean},
// This is used to cache urls that are not found
missingFile: {type: Object},
};
async _init() {
@@ -399,27 +420,12 @@ class App extends LitElement {
this.selectedTests = [];
this.comparisonContent = null;
this.compareMode = false;
this.missingFile = {
[getDiffUrl('undefined')]: true,
};
this._init();
this.addEventListener('dialog-closed', () => {
this.dialogContent = null;
this.comparisonContent = null;
});
this.addEventListener('url-hit', (ev) => {
delete this.missingFile[ev.detail.value];
this.missingFile = this.missingFile;
this.requestUpdate();
});
this.addEventListener('url-miss', (ev) => {
this.missingFile[ev.detail.value] = true;
this.missingFile = this.missingFile;
this.requestUpdate();
});
}
updated(props) {
@@ -464,24 +470,8 @@ class App extends LitElement {
}
render() {
const sortFn = (a, b) => {
const aparts = a.name.split('.');
const bparts = b.name.split('.');
// 0 = test names
// 1 = backend
// 2 = model
for (let i of [0, 2, 1]) {
if (aparts[i] < bparts[i]) {
return -1;
}
if (aparts[i] > bparts[i]) {
return 1;
}
}
return 0;
};
let passed = this.tests.filter((t) => t.result == 'ok').sort(sortFn);
let failed = this.tests.filter((t) => t.result != 'ok').sort(sortFn);
let passed = this.tests.filter((t) => t.result == 'ok');
let failed = this.tests.filter((t) => t.result != 'ok');
const singleTiff = (url) => {
return html`<tiff-viewer style="max-width:100px" fileurl="${url}"></tiff-viewer>`;
};
@@ -512,8 +502,7 @@ class App extends LitElement {
[goldenUrl, 'golden'],
[compUrl, 'rendered'],
[diffUrl, 'diff']
].filter((a) => !this.missingFile[a[0]])
.map((a) => [singleTiff(a[0]), a[1]])
].map((a) => [singleTiff(a[0]), a[1]])
.map((a) => wrap(...a));
return html`
<div class="test-item" @click="${(e)=>this._onClick(t, e)}" >

View File

@@ -30,20 +30,15 @@ export class TiffViewer extends LitElement {
static properties = {
fileurl: {type: String, attribute: 'fileurl'},
name: {type: String, attribute: 'name'},
failedToFetch: {type: Boolean },
};
constructor() {
super();
this.fileurl = null;
this.name = null;
this.failedToFetch = false;
}
render() {
if (this.failedToFetch) {
return html``;
}
return html`<canvas id="tiffCanvas"></canvas>`;
}
@@ -54,33 +49,7 @@ export class TiffViewer extends LitElement {
}
async _updateImage(fileurl) {
this.failedToFetch = false;
let fileblob = null;
try {
let res = await fetch(this.fileurl);
if (!res.ok) {
throw new Error(`Could not find ${this.fileurl}`);
}
fileblob = await res.arrayBuffer();
} catch (error) {
this.failedToFetch = true;
}
if (!fileblob) {
const event = new CustomEvent('url-miss', {
detail: { value: fileurl },
bubbles: true,
composed: true,
});
this.dispatchEvent(event);
return;
} else {
const event = new CustomEvent('url-hit', {
detail: { value: fileurl },
bubbles: true,
composed: true,
});
this.dispatchEvent(event);
}
const fileblob = await ((await fetch(this.fileurl)).arrayBuffer());
const canvas = this.shadowRoot.getElementById('tiffCanvas');
const ctx = canvas.getContext('2d');

View File

@@ -1,13 +0,0 @@
blinker==1.9.0
certifi==2025.8.3
charset-normalizer==3.4.3
click==8.2.1
Flask==3.1.2
idna==3.10
itsdangerous==2.2.0
Jinja2==3.1.6
MarkupSafe==3.0.2
requests==2.32.5
urllib3==2.5.0
waitress==3.0.2
Werkzeug==3.1.3

View File

@@ -1,30 +1,26 @@
{
"name": "presubmit",
"backends": ["opengl", "vulkan", "webgpu"],
"model_search_paths": ["third_party/models", "gltf"],
"backends": ["opengl", "vulkan"],
"model_search_paths": ["third_party/models"],
"presets": [
{
"name": "base",
"models": ["Box", "BoxTextured", "Duck", "lucy", "FlightHelmet"],
"models": ["lucy", "DamagedHelmet"],
"rendering": {
"viewer.cameraFocusDistance": 0,
"view.postProcessingEnabled": true,
"view.dithering": "NONE"
}
},
{
"name": "helmet_only",
"models": ["DamagedHelmet"],
"rendering": {}
}
],
"tests": [
{
"name": "Bloom",
"description": "Testing bloom",
"apply_presets": ["base", "helmet_only"],
"name": "BloomFlare",
"description": "Testing bloom and flare",
"apply_presets": ["base"],
"rendering": {
"view.bloom.enabled": true
"view.bloom.enabled": true,
"view.bloom.lensFlare": true
}
},
{

View File

@@ -1,6 +1,6 @@
{
"name": "filament",
"version": "1.65.2",
"version": "1.65.0",
"description": "Real-time physically based rendering engine",
"main": "filament.js",
"module": "filament.js",