Compare commits

..
Author SHA1 Message Date
Sungun Park 07356a2949 tmp - testing edge cases 2025-06-02 20:02:21 -07:00
42 changed files with 271 additions and 1153 deletions
-33
View File
@@ -1,33 +0,0 @@
name: 'Get commit message'
outputs:
msg:
value: ${{ steps.action_output.outputs.msg }}
runs:
using: "composite"
steps:
- name: Find commit message (on push)
if: github.event_name == 'push'
shell: bash
run: |
echo "${{ github.event.head_commit.message }}" >> /tmp/commit_msg.txt
- name: Checkout code
shell: bash
id: checkout_code
if: github.event_name == 'pull_request'
run: |
echo "hash=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
git checkout ${{ github.event.pull_request.head.ref }}
COMMIT_MESSAGE=$(git log -1)
echo "${COMMIT_MESSAGE}" >> /tmp/commit_msg.txt
- shell: bash
id: action_output
run: |
DELIMITER="EOF_FILE_CONTENT_$(date +%s)" # Using timestamp to make it more unique
echo "msg<<$DELIMITER" >> "$GITHUB_OUTPUT"
cat /tmp/commit_msg.txt >> "$GITHUB_OUTPUT"
echo "$DELIMITER" >> "$GITHUB_OUTPUT"
- name: Uncheckout code
shell: bash
if: github.event_name == 'pull_request'
run: |
git checkout ${{ steps.checkout.outputs.hash }}
-35
View File
@@ -1,35 +0,0 @@
name: 'Post-submit tasks'
on:
push:
branches:
- main
jobs:
update-renderdiff-goldens:
name: update-renderdiff-goldens
runs-on: 'ubuntu-24.04-4core'
steps:
- uses: actions/checkout@v4.1.6
with:
fetch-depth: 0
- uses: ./.github/actions/linux-prereq
- id: get_commit_msg
uses: ./.github/actions/get-commit-msg
- name: Prerequisites
run: pip install tifffile numpy
- name: Run update script
env:
GH_TOKEN: ${{ secrets.FILAMENTBOT_TOKEN }}
run: |
GOLDEN_BRANCH=$(echo "${{ steps.get_commit_msg.outputs.msg }}" | python3 test/renderdiff/src/commit_msg.py)
COMMIT_HASH=$(echo "${{ steps.get_commit_msg.outputs.msg }}" | head -n 1 | tr -d 'commit ')
if [[ "${GOLDEN_BRANCH}" != "main" ]]; then
git config --global user.email "filament.bot@gmail.com"
git config --global user.name "Filament Bot"
git config --global credential.helper cache
echo "branch==${GOLDEN_BRANCH}"
echo "hash==${COMMIT_HASH}"
python3 test/renderdiff/src/update_golden.py --branch=${GOLDEN_BRANCH} \
--merge-to-main --filament-tag=${COMMIT_HASH} --golden-repo-token=${GH_TOKEN}
fi
+10 -13
View File
@@ -3,10 +3,10 @@ name: Presubmit
on:
push:
branches:
- main
- main
pull_request:
branches:
- main
- main
jobs:
build-desktop-mac:
@@ -41,7 +41,8 @@ jobs:
build-windows:
name: build-windows
runs-on: windows-2022-32core
runs-on: win-2019-16core
steps:
- uses: actions/checkout@v4.1.6
with:
@@ -109,35 +110,31 @@ jobs:
fetch-depth: 0
- name: Check for manual edits to /docs
run: |
echo "${{ github.event.pull_request.head.sha }} -- ${{ github.event.pull_request.head.sha }}"
# disable for now
# bash docs_src/build/presubmit_check.sh ${{ github.event.pull_request.head.sha }}
echo "${{ github.event.pull_request.head.sha }} -- ${{ github.event.pull_request.head.sha }}"
# disable for now
# bash docs_src/build/presubmit_check.sh ${{ github.event.pull_request.head.sha }}
test-renderdiff:
name: test-renderdiff
runs-on: macos-14-xlarge
steps:
- uses: actions/checkout@v4.1.6
with:
fetch-depth: 0
- id: get_commit_msg
uses: ./.github/actions/get-commit-msg
- uses: actions/checkout@v4.1.6
with:
fetch-depth: 0
- uses: ./.github/actions/mac-prereq
- name: Cache Mesa and deps
id: mesa-cache
uses: actions/cache@v4
with:
path: mesa
key: ${{ runner.os }}-mesa-deps-2-${{ vars.MESA_VERSION }}
- name: Prerequisites
id: prereqs
run: |
bash build/common/get-mesa.sh
pip install tifffile numpy
- name: Run Test
run: |
echo "${{ steps.get_commit_msg.outputs.msg }}" | bash test/renderdiff/test.sh
run: bash test/renderdiff/test.sh
- uses: actions/upload-artifact@v4
with:
name: presubmit-renderdiff-result
+1 -1
View File
@@ -226,7 +226,7 @@ jobs:
build-windows:
name: build-windows
runs-on: windows-2022-32core
runs-on: windows-2019-32core
if: github.event_name == 'release' || github.event.inputs.platform == 'windows'
steps:
+1 -1
View File
@@ -10,7 +10,7 @@ on:
jobs:
build-windows:
name: build-windows
runs-on: windows-2022-32core
runs-on: windows-2019-32core
steps:
- uses: actions/checkout@v4.1.6
+2 -2
View File
@@ -31,7 +31,7 @@ repositories {
}
dependencies {
implementation 'com.google.android.filament:filament-android:1.61.1'
implementation 'com.google.android.filament:filament-android:1.60.1'
}
```
@@ -51,7 +51,7 @@ Here are all the libraries available in the group `com.google.android.filament`:
iOS projects can use CocoaPods to install the latest release:
```shell
pod 'Filament', '~> 1.61.1'
pod 'Filament', '~> 1.60.1'
```
## Documentation
-3
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.61.1
## v1.61.0
- materials: sampler now export their type in the material binary [⚠️ **New Material Version**]
+1 -1
View File
@@ -1,5 +1,5 @@
GROUP=com.google.android.filament
VERSION_NAME=1.61.1
VERSION_NAME=1.60.1
POM_DESCRIPTION=Real-time physically based rendering engine for Android.
+2 -9
View File
@@ -41,8 +41,6 @@ for cmd in "${NEEDED_PYTHON_DEPS[@]}"; do
done
deactivate
LOCAL_PKG_CONFIG_PATH=
# Install system deps
if [[ "$OS_NAME" == "Linux" ]]; then
if [[ "$GITHUB_WORKFLOW" ]]; then
@@ -84,9 +82,6 @@ elif [[ "$OS_NAME" == "Darwin" ]]; then
fi
fi
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=true brew install autoconf automake libx11 libxext libxrandr llvm@${LLVM_VERSION} ninja meson pkg-config libxshmfence
# For reasons unknown, this is necessary for pkg-config to find homebrew's packages
LOCAL_PKG_CONFIG_PATH="/opt/homebrew/lib/pkgconfig:$PKG_CONFIG_PATH"
fi # [[ "$OS_NAME" == x ]]
LOCAL_LDFLAGS=${LDFLAGS}
@@ -139,11 +134,9 @@ fi
# -Dgallium-drivers=swrast => builds GL software rasterizer
# -Dvulkan-drivers=swrast => builds VK software rasterizer
# -Dgallium-drivers=llvmpipe is needed for GL >= 4.1 pipe-screen (see src/gallium/auxiliary/target-helpers/inline_sw_helper.h)
PKG_CONFIG_PATH=${LOCAL_PKG_CONFIG_PATH} PATH=${LOCAL_PATH} \
CXX=${LOCAL_CXX} CC=${LOCAL_CC} LDFLAGS=${LOCAL_LDFLAGS} CPPFLAGS=${LOCAL_CPPFLAGS} \
CXX=${LOCAL_CXX} CC=${LOCAL_CC} PATH=${LOCAL_PATH} LDFLAGS=${LOCAL_LDFLAGS} CPPFLAGS=${LOCAL_CPPFLAGS} \
meson setup --wipe builddir/ -Dprefix="${MESA_DIR}/out" -Dglx=xlib -Dosmesa=true -Dgallium-drivers=llvmpipe,swrast -Dvulkan-drivers=swrast
PKG_CONFIG_PATH=${LOCAL_PKG_CONFIG_PATH} PATH=${LOCAL_PATH} \
CXX=${LOCAL_CXX} CC=${LOCAL_CC} LDFLAGS=${LOCAL_LDFLAGS} CPPFLAGS=${LOCAL_CPPFLAGS} \
CXX=${LOCAL_CXX} CC=${LOCAL_CC} PATH=${LOCAL_PATH} LDFLAGS=${LOCAL_LDFLAGS} CPPFLAGS=${LOCAL_CPPFLAGS} \
meson install -C builddir/
# Disable python venv
+2 -6
View File
@@ -47,11 +47,7 @@ if "%RUNNING_LOCALLY%" == "1" (
set "PATH=%PATH%;C:\Program Files\7-Zip"
)
:: Outdated windows-2019 pattern
:: call "C:\Program Files (x86)\Microsoft Visual Studio\2019\%VISUAL_STUDIO_VERSION%\VC\Auxiliary\Build\vcvars64.bat"
call "C:\Program Files\Microsoft Visual Studio\2022\%VISUAL_STUDIO_VERSION%\VC\Auxiliary\Build\vcvars64.bat"
echo Passed vcvars64.bat
call "C:\Program Files (x86)\Microsoft Visual Studio\2019\%VISUAL_STUDIO_VERSION%\VC\Auxiliary\Build\vcvars64.bat"
if errorlevel 1 exit /b %errorlevel%
msbuild /version
@@ -111,7 +107,7 @@ cd out\cmake-%variant%
if errorlevel 1 exit /b %errorlevel%
cmake ..\.. ^
-G "Visual Studio 17 2022" ^
-G "Visual Studio 16 2019" ^
-A x64 ^
%flag% ^
-DCMAKE_INSTALL_PREFIX=..\%variant% ^
@@ -222,6 +222,56 @@ public:
return static_cast<Dp>(p);
}
template<typename Dp, typename B>
inline std::enable_if_t<
std::is_pointer_v<Dp> && std::is_base_of_v<B, std::remove_pointer_t<Dp>>, int16_t>
handle_set_value(Handle<B>& handle, uint8_t v) {
assert_invariant(handle);
auto [p, tag] = handleToPointer(handle.getId());
if (isPoolHandle(handle.getId())) {
// check for pool handle use-after-free
if (UTILS_UNLIKELY(!mUseAfterFreeCheckDisabled)) {
auto const pNode = static_cast<typename Allocator::Node*>(p);
pNode[-1].test_value = v;
return 1;
}
return 0;
} else {
// check for heap handle use-after-free
if (UTILS_UNLIKELY(!mUseAfterFreeCheckDisabled)) {
return -1;
}
return -2;
}
}
template<typename Dp, typename B>
inline std::enable_if_t<
std::is_pointer_v<Dp> && std::is_base_of_v<B, std::remove_pointer_t<Dp>>, int16_t>
handle_get_value(Handle<B>& handle) {
assert_invariant(handle);
auto [p, tag] = handleToPointer(handle.getId());
if (isPoolHandle(handle.getId())) {
// check for pool handle use-after-free
if (UTILS_UNLIKELY(!mUseAfterFreeCheckDisabled)) {
auto const pNode = static_cast<typename Allocator::Node*>(p);
uint8_t age = pNode[-1].age;
uint8_t test_value = pNode[-1].test_value;
int16_t ret = (int16_t) ((age << 8) | test_value);
return ret;
}
return 0;
} else {
// check for heap handle use-after-free
if (UTILS_UNLIKELY(!mUseAfterFreeCheckDisabled)) {
return -1;
}
return -2;
}
}
utils::CString getHandleTag(HandleBase::HandleId key) const noexcept;
template<typename B>
@@ -276,7 +326,10 @@ private:
class Allocator {
friend class HandleAllocator;
static constexpr size_t MIN_ALIGNMENT = alignof(std::max_align_t);
struct Node { uint8_t age; };
struct Node {
uint8_t age;
uint8_t test_value = 0xff;
};
// Note: using the `extra` parameter of PoolAllocator<>, even with a 1-byte structure,
// generally increases all pool allocations by 8-bytes because of alignment restrictions.
template<size_t SIZE>
+78 -20
View File
@@ -643,6 +643,25 @@ void OpenGLDriver::createVertexBufferInfoR(
AttributeArray attributes) {
DEBUG_MARKER()
construct<GLVertexBufferInfo>(vbih, bufferCount, attributeCount, attributes);
// test ----
int16_t returned_value = handle_set_value<GLVertexBufferInfo*>(vbih, 90);
uint8_t age = (uint8_t) (returned_value >> 8);
uint8_t test_value = (returned_value & 0xff);
switch (test_value) {
case -2:
case -1:
case 0:
utils::slog.w << "@@@ createVertexBufferInfoR: unexpected result, age:" << (int) age
<< ", test_value:" << (int) test_value << utils::io::endl;
break;
default:
FILAMENT_CHECK_POSTCONDITION(test_value == 1)
<< "@@@ createVertexBufferInfoR: shouldn't happen, age:" << (int) age
<< ", test_value:" << (int) test_value << utils::io::endl;
break;
}
//----------
}
void OpenGLDriver::createVertexBufferR(
@@ -761,6 +780,25 @@ void OpenGLDriver::createProgramR(Handle<HwProgram> ph, Program&& program) {
construct<OpenGLProgram>(ph, *this, std::move(program));
CHECK_GL_ERROR(utils::slog.e)
// test ----
int16_t returned_value = handle_set_value<OpenGLProgram*>(ph, 100);
uint8_t age = (uint8_t) (returned_value >> 8);
uint8_t test_value = (returned_value & 0xff);
switch (test_value) {
case -2:
case -1:
case 0:
utils::slog.w << "@@@ createProgramR: unexpected result, age:" << (int)age << ", test_value:" << (int)test_value
<< utils::io::endl;
break;
default:
FILAMENT_CHECK_POSTCONDITION(test_value == 1)
<< "@@@ createProgramR: shouldn't happen, age:" << (int)age
<< ", test_value:" << (int)test_value << utils::io::endl;
break;
}
//----------
}
UTILS_NOINLINE
@@ -1918,6 +1956,25 @@ void OpenGLDriver::destroyRenderPrimitive(Handle<HwRenderPrimitive> rph) {
void OpenGLDriver::destroyProgram(Handle<HwProgram> ph) {
DEBUG_MARKER()
if (ph) {
// test ----
int16_t returned_value = handle_set_value<OpenGLProgram*>(ph, 101);
uint8_t age = (uint8_t) (returned_value >> 8);
uint8_t test_value = (returned_value & 0xff);
switch (test_value) {
case -2:
case -1:
case 0:
utils::slog.w << "@@@ destroyProgram: unexpected result, age:" << (int) age
<< ", test_value:" << (int) test_value << utils::io::endl;
break;
default:
FILAMENT_CHECK_POSTCONDITION(test_value == 1)
<< "@@@ destroyProgram: shouldn't happen, age:" << (int) age
<< ", test_value:" << (int) test_value << utils::io::endl;
break;
}
//----------
OpenGLProgram* p = handle_cast<OpenGLProgram*>(ph);
destruct(ph, p);
}
@@ -2542,12 +2599,7 @@ void OpenGLDriver::makeCurrent(Handle<HwSwapChain> schDraw, Handle<HwSwapChain>
[this](size_t index) {
for (auto t: mTexturesWithStreamsAttached) {
if (t->hwStream->streamType == StreamType::NATIVE) {
if (t->externalTexture) {
glGenTextures(1, &t->externalTexture->id);
t->gl.id = t->externalTexture->id;
} else {
glGenTextures(1, &t->gl.id);
}
glGenTextures(1, &t->gl.id);
mPlatform.attach(t->hwStream->stream, t->gl.id);
mContext.updateTexImage(GL_TEXTURE_EXTERNAL_OES, t->gl.id);
}
@@ -3032,7 +3084,6 @@ void OpenGLDriver::attachStream(GLTexture* t, GLStream* hwStream) noexcept {
switch (hwStream->streamType) {
case StreamType::NATIVE:
mPlatform.attach(hwStream->stream, t->gl.id);
mContext.updateTexImage(GL_TEXTURE_EXTERNAL_OES, t->gl.id);
break;
case StreamType::ACQUIRED:
break;
@@ -3061,12 +3112,7 @@ void OpenGLDriver::detachStream(GLTexture* t) noexcept {
break;
}
if (t->externalTexture) {
glGenTextures(1, &t->externalTexture->id);
t->gl.id = t->externalTexture->id;
} else {
glGenTextures(1, &t->gl.id);
}
glGenTextures(1, &t->gl.id);
t->hwStream = nullptr;
}
@@ -3090,14 +3136,8 @@ void OpenGLDriver::replaceStream(GLTexture* texture, GLStream* newStream) noexce
switch (newStream->streamType) {
case StreamType::NATIVE:
if (texture->externalTexture) {
glGenTextures(1, &texture->externalTexture->id);
texture->gl.id = texture->externalTexture->id;
} else {
glGenTextures(1, &texture->gl.id);
}
glGenTextures(1, &texture->gl.id);
mPlatform.attach(newStream->stream, texture->gl.id);
mContext.updateTexImage(GL_TEXTURE_EXTERNAL_OES, texture->gl.id);
break;
case StreamType::ACQUIRED:
// Just re-use the old texture id.
@@ -4083,6 +4123,24 @@ void OpenGLDriver::bindPipeline(PipelineState const& state) {
setRasterState(state.rasterState);
setStencilState(state.stencilState);
gl.polygonOffset(state.polygonOffset.slope, state.polygonOffset.constant);
// test ----
Handle<HwProgram> pro = state.program;
int16_t returned_value = handle_get_value<OpenGLProgram*>(pro);
uint8_t age = (uint8_t)(returned_value >> 8);
uint8_t test_value = (returned_value & 0xff);
switch (test_value) {
case -2:
case -1:
case 0:
utils::slog.w << "@@@ bindPipeline: unexpected result, age:" << (int)age << ", test_value:" << (int)test_value << utils::io::endl;
break;
default:
if (test_value != 100) {
utils::slog.w << "@@@ bindPipeline: program is not valid OpenGLProgram, age:" << (int)age << ", test_value:" << (int)test_value << utils::io::endl;
}
break;
}
//----------
OpenGLProgram* const p = handle_cast<OpenGLProgram*>(state.program);
mValidProgram = useProgram(p);
(*mCurrentPushConstants) = p->getPushConstants();
@@ -282,6 +282,20 @@ private:
return mHandleAllocator.handle_cast<Dp, B>(handle);
}
template<typename Dp, typename B>
std::enable_if_t<std::is_pointer_v<Dp> && std::is_base_of_v<B, std::remove_pointer_t<Dp>>,
int16_t>
handle_set_value(Handle<B>& handle, uint8_t v) {
return mHandleAllocator.handle_set_value<Dp, B>(handle, v);
}
template<typename Dp, typename B>
std::enable_if_t<std::is_pointer_v<Dp> && std::is_base_of_v<B, std::remove_pointer_t<Dp>>,
int16_t>
handle_get_value(Handle<B>& handle) {
return mHandleAllocator.handle_get_value<Dp, B>(handle);
}
friend class OpenGLProgram;
friend class ShaderCompilerService;
@@ -23,7 +23,6 @@
#include "OpenGLDriver.h"
#include <iterator>
#include <optional>
#include <private/backend/BackendUtils.h>
#include <backend/DriverEnums.h>
@@ -111,17 +110,7 @@ struct ShaderCompilerService::OpenGLProgramToken : ProgramToken {
cond.wait(l, [this] { return signaled; });
}
// This is invoked upon token completion, which occurs after a successful `gl.program`
// population or upon cancellation. In either scenario, the callback handle must be submitted
// to notify the caller that resource loading has concluded.
void trySubmittingCallback() noexcept {
if (handle) {
compiler.submitCallbackHandle(*handle);
handle = std::nullopt;
}
}
std::optional<CallbackManager::Handle> handle{};
CallbackManager::Handle handle{};
BlobCacheKey key;
// Used for the `THREAD_POOL` mode.
@@ -131,7 +120,7 @@ struct ShaderCompilerService::OpenGLProgramToken : ProgramToken {
};
ShaderCompilerService::OpenGLProgramToken::~OpenGLProgramToken() {
trySubmittingCallback();
compiler.submitCallbackHandle(handle);
}
/* static */ void ShaderCompilerService::setUserData(const program_token_t& token,
@@ -350,7 +339,7 @@ GLuint ShaderCompilerService::getProgram(program_token_t& token) {
// Cleanup the token.
token->compiler.cancelTickOp(token);
token = nullptr; // This will try submitting a callback handle to the callback manager.
token = nullptr;// This will submit a callback condition (handle) to the callback manager.
}
void ShaderCompilerService::tick() {
@@ -403,7 +392,7 @@ GLuint ShaderCompilerService::initialize(program_token_t& token) {
// Cleanup the token.
token->compiler.cancelTickOp(token);
token = nullptr;
token = nullptr;// This will submit a callback condition (handle) to the callback manager.
return program;
}
@@ -670,7 +659,6 @@ void ShaderCompilerService::executeTickOps() noexcept {
}
glLinkProgram(program);
token->gl.program = program;
token->trySubmittingCallback();
}
/* static */ bool ShaderCompilerService::isLinkCompleted(program_token_t const& token) noexcept {
+5 -6
View File
@@ -950,12 +950,11 @@ void WebGPUDriver::bindPipeline(PipelineState const& pipelineState) {
pipelineSamples = mCurrentRenderTarget->getSamples();
}
// 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.";
//}
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,
@@ -536,8 +536,8 @@ size_t WebGPUDescriptorSet::countEntitiesWithDynamicOffsets() const {
return mEntriesWithDynamicOffsetsCount;
}
WGPUTexture::WGPUTexture(SamplerType samplerTargetType, uint8_t levels, TextureFormat format,
uint8_t samples, uint32_t width, uint32_t height, uint32_t depth, TextureUsage usage,
WGPUTexture::WGPUTexture(SamplerType target, uint8_t levels, TextureFormat format, uint8_t samples,
uint32_t width, uint32_t height, uint32_t depth, TextureUsage usage,
wgpu::Device const& device) noexcept {
assert_invariant(
samples == 1 ||
@@ -552,8 +552,6 @@ WGPUTexture::WGPUTexture(SamplerType samplerTargetType, uint8_t levels, TextureF
mAspect = fToWGPUTextureViewAspect(usage, format);
mBlockWidth = filament::backend::getBlockWidth(format);
mBlockHeight = filament::backend::getBlockHeight(format);
target = samplerTargetType;
wgpu::TextureDescriptor textureDescriptor{
.label = getUserTextureLabel(target),
.usage = mUsage,
@@ -603,7 +601,6 @@ WGPUTexture::WGPUTexture(WGPUTexture* src, uint8_t baseLevel, uint8_t levelCount
mAspect = src->mAspect;
mBlockWidth = src->mBlockWidth;
mBlockHeight = src->mBlockHeight;
target = src->target;
mTexureView = makeTextureView(baseLevel, levelCount, target);
}
@@ -903,7 +900,7 @@ wgpu::TextureAspect WGPUTexture::fToWGPUTextureViewAspect(TextureUsage const& fU
const bool isDepth = any(fUsage & TextureUsage::DEPTH_ATTACHMENT);
const bool isStencil = any(fUsage & TextureUsage::STENCIL_ATTACHMENT);
const bool isColor = any(fUsage & TextureUsage::COLOR_ATTACHMENT);
const bool isSample = any(fUsage & TextureUsage::SAMPLEABLE);
const bool isSample = (fUsage == TextureUsage::SAMPLEABLE);
if (isDepth && !isColor && !isStencil) {
return wgpu::TextureAspect::DepthOnly;
+2 -2
View File
@@ -169,8 +169,8 @@ private:
class WGPUTexture : public HwTexture {
public:
WGPUTexture(SamplerType samplerTargetType, uint8_t levels, TextureFormat format,
uint8_t samples, uint32_t width, uint32_t height, uint32_t depth, TextureUsage usage,
WGPUTexture(SamplerType target, uint8_t levels, TextureFormat format, uint8_t samples,
uint32_t width, uint32_t height, uint32_t depth, TextureUsage usage,
wgpu::Device const& device) noexcept;
WGPUTexture(WGPUTexture* src, uint8_t baseLevel, uint8_t levelCount) noexcept;
@@ -237,10 +237,9 @@ wgpu::RenderPipeline createWebGPURenderPipeline(wgpu::Device const& device,
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.
// 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);
assert_invariant(fragmentState.targetCount > 0);
for (size_t targetIndex = 0; targetIndex < fragmentState.targetCount; targetIndex++) {
auto& colorTarget = colorTargets[targetIndex];
colorTarget.format = colorFormats[targetIndex];
+3 -6
View File
@@ -629,8 +629,7 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateCascadeShadowMaps(FEng
.textureDimension = uint16_t(options.mapSize),
.shadowDimension = uint16_t(options.mapSize - 2u),
.textureSpaceFlipped = engine.getBackend() == Backend::METAL ||
engine.getBackend() == Backend::VULKAN ||
engine.getBackend() == Backend::WEBGPU,
engine.getBackend() == Backend::VULKAN,
.vsm = view.hasVSM()
};
@@ -807,8 +806,7 @@ void ShadowMapManager::prepareSpotShadowMap(ShadowMap& shadowMap, FEngine& engin
.textureDimension = uint16_t(options->mapSize),
.shadowDimension = uint16_t(options->mapSize - 2u),
.textureSpaceFlipped = engine.getBackend() == Backend::METAL ||
engine.getBackend() == Backend::VULKAN ||
engine.getBackend() == Backend::WEBGPU,
engine.getBackend() == Backend::VULKAN,
.vsm = view.hasVSM()
};
@@ -900,8 +898,7 @@ void ShadowMapManager::preparePointShadowMap(ShadowMap& shadowMap,
.textureDimension = uint16_t(options->mapSize),
.shadowDimension = uint16_t(options->mapSize), // point-lights don't have a border
.textureSpaceFlipped = engine.getBackend() == Backend::METAL ||
engine.getBackend() == Backend::VULKAN ||
engine.getBackend() == Backend::WEBGPU,
engine.getBackend() == Backend::VULKAN,
.vsm = view.hasVSM()
};
+1 -1
View File
@@ -429,7 +429,7 @@ void FMaterialInstance::fixMissingSamplers() const {
FEngine const& engine = mMaterial->getEngine();
filament::DescriptorSetLayout const& layout = mMaterial->getDescriptorSetLayout();
if (pos->format == SamplerFormat::FLOAT) {
if (pos->format != SamplerFormat::FLOAT) {
// TODO: we only handle missing samplers that are FLOAT
switch (pos->type) {
case SamplerType::SAMPLER_2D:
+2 -2
View File
@@ -1,12 +1,12 @@
Pod::Spec.new do |spec|
spec.name = "Filament"
spec.version = "1.61.1"
spec.version = "1.60.1"
spec.license = { :type => "Apache 2.0", :file => "LICENSE" }
spec.homepage = "https://google.github.io/filament"
spec.authors = "Google LLC."
spec.summary = "Filament is a real-time physically based rendering engine for Android, iOS, Windows, Linux, macOS, and WASM/WebGL."
spec.platform = :ios, "11.0"
spec.source = { :http => "https://github.com/google/filament/releases/download/v1.61.1/filament-v1.61.1-ios.tgz" }
spec.source = { :http => "https://github.com/google/filament/releases/download/v1.60.1/filament-v1.60.1-ios.tgz" }
# Fix linking error with Xcode 12; we do not yet support the simulator on Apple silicon.
spec.pod_target_xcconfig = {
@@ -28,7 +28,7 @@
namespace filament {
// update this when a new version of filament wouldn't work with older materials
static constexpr size_t MATERIAL_VERSION = 61;
static constexpr size_t MATERIAL_VERSION = 60;
/**
* Supported shading models
+2 -2
View File
@@ -30,9 +30,9 @@
# define FILAMENT_APPLE_SYSTRACE 0
#endif
#if defined(__ANDROID__) && FILAMENT_TRACING_ENABLED
#if defined(__ANDROID__)
# include <private/utils/android/Tracing.h>
#elif defined(__APPLE__) && FILAMENT_APPLE_SYSTRACE && FILAMENT_TRACING_ENABLED
#elif defined(__APPLE__) && FILAMENT_APPLE_SYSTRACE
# include <private/utils/darwin/Tracing.h>
#else
-6
View File
@@ -14,13 +14,8 @@
* limitations under the License.
*/
// TODO: Clean-up. We shouldn't need this #ifndef here, but a client has requested that perfetto be
// disabled due to size increase. In their case, this flag would be defined across targets. Hence
// we guard below with an #ifndef.
#ifndef FILAMENT_TRACING_ENABLED
// Note: The overhead of TRACING is not negligible especially with parallel_for().
#define FILAMENT_TRACING_ENABLED false
#endif
// when FILAMENT_TRACING_ENABLED is true, enables even heavier tracing
#define HEAVY_TRACING 0
@@ -64,7 +59,6 @@
// see https://developer.android.com/topic/performance/threads#priority
# include <sys/time.h>
# include <sys/resource.h>
# include <unistd.h>
# ifndef ANDROID_PRIORITY_URGENT_DISPLAY
# define ANDROID_PRIORITY_URGENT_DISPLAY (-8)
# endif
+2 -2
View File
@@ -19,8 +19,8 @@ void main() {
#endif
// Adjust clip-space
#if !defined(TARGET_VULKAN_ENVIRONMENT) && !defined(TARGET_METAL_ENVIRONMENT) && !defined(TARGET_WEBGPU_ENVIRONMENT)
// This is not needed in Vulkan, Metal or WebGPU because clipControl is always (1, 0)
#if !defined(TARGET_VULKAN_ENVIRONMENT) && !defined(TARGET_METAL_ENVIRONMENT)
// This is not needed in Vulkan or Metal because clipControl is always (1, 0)
// (We don't use a dot() here because it works around a spirv-opt optimization that in turn
// causes a crash on PowerVR, see #5118)
inputs.position.z = inputs.position.z * frameUniforms.clipControl.x + inputs.position.w * frameUniforms.clipControl.y;
+2 -2
View File
@@ -246,8 +246,8 @@ void main() {
position.y = -position.y;
#endif
#if !defined(TARGET_VULKAN_ENVIRONMENT) && !defined(TARGET_METAL_ENVIRONMENT) && !defined(TARGET_WEBGPU_ENVIRONMENT)
// This is not needed in Vulkan, Metal or WebGPU because clipControl is always (1, 0)
#if !defined(TARGET_VULKAN_ENVIRONMENT) && !defined(TARGET_METAL_ENVIRONMENT)
// This is not needed in Vulkan or Metal because clipControl is always (1, 0)
// (We don't use a dot() here because it workaround a spirv-opt optimization that in turn
// causes a crash on PowerVR, see #5118)
position.z = position.z * frameUniforms.clipControl.x + position.w * frameUniforms.clipControl.y;
+3 -64
View File
@@ -7,72 +7,11 @@ machines. To perform software rasterization, these scripts are centered around [
rasterizers, but nothing bars us from using another rasterizer like [SwiftShader]. Additionally,
we should be able to use GPUs where available (though this is more of a future work).
The script `render.py` contains the core logic for taking input parameters (such as the test
description file) and then running gltf_viewer to produce the renderings.
The script `run.py` contains the core logic for taking input parameters (such as the test
description file) and then running gltf_viewer to produce the results.
In the `test` directory is a list of test descriptions that are specified in json. Please see
`sample.json` to parse the structure.
## Running the test locally
- To run the same presbumit as [`test-renderdiff`][presubmit-renderdiff], you can do
```
bash test/renderdiff/test.sh
```
- This script will generate the renderings based on the current state of your repo.
Additionally, it will also compare the generated images with corresponding images in the
golden repo.
- To just render without running the test, you could use the following script
```
bash test/renderdiff/generate.sh
```
## Update the golden images
The golden images are stored in a github repository: https://github.com/google/filament-assets.
Filament team members should have access to write to the repository. A typical flow for updating
the goldens is to upload your changed images into **branch** of `filament-assets`. This branch is
paired with a PR or commit on the `filament` repo.
As an example, imagine I am working on a PR, and I've uploaded my change, which is in a branch
called `my-pr-branch`, to `filament`. This PR requires updating the golden. We would do it
in the following fashion
### Using a script to update the golden repo
- Run interactive mode in the `update_golden.py` script.
```
python3 test/renderdiff/src/update_golden.py
```
- This will guide you through a series of steps to push the changes to a remote branch
on `filament-assets`.
### Manually updating the golden repo
- Check out the golden repo
```
git clone git@github.com:google/filament-assets.git
```
- Create a branch on the golden repo
```
cd filament-assets
git switch -c my-pr-branch-golden
```
- 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
```
- In the commit message of your working branch on `filament`, add the following line
```
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.
[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
[SwiftShader]: https://github.com/google/swiftshader
+2 -1
View File
@@ -34,6 +34,7 @@ function start_render_() {
fi
done
fi
mkdir -p ${OUTPUT_DIR}
CXX=`which clang++` CC=`which clang` ./build.sh -f -X ${MESA_DIR} -p desktop debug gltf_viewer
}
@@ -53,6 +54,6 @@ start_render_ && \
python3 ${RENDERDIFF_TEST_DIR}/src/render.py \
--gltf_viewer="$(pwd)/out/cmake-debug/samples/gltf_viewer" \
--test=${RENDERDIFF_TEST_DIR}/tests/presubmit.json \
--output_dir=${RENDER_OUTPUT_DIR} \
--output_dir=${OUTPUT_DIR} \
--opengl_lib=${MESA_LIB_DIR} && \
end_render_
+25 -48
View File
@@ -4,54 +4,26 @@ import sys
import pprint
import json
from utils import execute, ArgParseImpl, important_print, mkdir_p
from image_diff import same_image, output_image_diff
from utils import execute, ArgParseImpl, important_print
from image_diff import same_image
from results import RESULT_OK, RESULT_FAILED, RESULT_MISSING
def _compare_goldens(base_dir, comparison_dir, out_dir=None):
all_files = glob.glob(os.path.join(base_dir, "./**/*.tif"), recursive=True)
test_dirs = set(os.path.abspath(os.path.dirname(f)).replace(os.path.abspath(base_dir) + '/', '') \
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)
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)
test_case = base_fname.replace(f'{base_test_dir}/', '')
comp_fname = os.path.join(comp_test_dir, test_case)
result = {
'name': test_case,
}
if not os.path.exists(comp_fname):
print(f'file name not found: {comp_fname}')
result['result'] = RESULT_MISSING
elif not same_image(base_fname, comp_fname):
result['result'] = RESULT_FAILED
if output_test_dir:
# just the file name
diff_fname = f"{test_case.replace('.tif', '_diff.tif')}"
output_image_diff(base_fname, comp_fname, os.path.join(output_test_dir, diff_fname))
result['diff'] = diff_fname
else:
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")
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}')
all_results += results
return all_results
def _compare_goldens(base_dir, comparison_dir):
render_results = {}
base_files = glob.glob(os.path.join(base_dir, "./**/*.tif"))
for golden_file in base_files:
base_fname = os.path.abspath(golden_file)
test_case = base_fname.replace(f'{os.path.abspath(base_dir)}/', '')
comp_fname = os.path.abspath(os.path.join(comparison_dir, test_case))
if not os.path.exists(comp_fname):
print(f'file name not found: {comp_fname}')
render_results[test_case] = RESULT_MISSING
continue
if not same_image(base_fname, comp_fname):
render_results[test_case] = RESULT_FAILED
else:
render_results[test_case] = RESULT_OK
return render_results
if __name__ == '__main__':
parser = ArgParseImpl()
@@ -67,9 +39,14 @@ if __name__ == '__main__':
dest = os.path.join(os.getcwd(), './out/renderdiff_tests')
assert os.path.exists(dest), f"Destination folder={dest} does not exist."
results = _compare_goldens(args.src, dest, out_dir=args.out)
results = _compare_goldens(args.src, dest)
failed = [f" {k['name']}" for k in results if k['result'] != RESULT_OK]
if args.out:
assert os.path.exists(arg.out), f"Output folder={dest} does not exist."
with open(os.path.join(args.out, "compare_results.json", 'w')) as f:
f.write(json.dumps(results))
failed = [f" {k}" for k in results.keys() if results[k] != RESULT_OK]
success_count = len(results) - len(failed)
important_print(f'Successfully compared {success_count} / {len(results)} images' +
('\nFailed:\n' + ('\n'.join(failed)) if len(failed) > 0 else ''))
+5 -18
View File
@@ -65,7 +65,7 @@ class GoldenManager:
assets_dir = self._assets_dir()
if not os.path.exists(assets_dir):
execute(
f'git clone {self._get_repo_url()}',
f'git clone --depth=1 {self._get_repo_url()}',
cwd=self.working_dir_,
capture_output=False
)
@@ -83,26 +83,13 @@ class GoldenManager:
self._git_exec('rebase')
def _git_exec(self, cmd):
return execute(f'git {cmd}', cwd=self._assets_dir(), capture_output=False)
execute(f'git {cmd}', cwd=self._assets_dir(), capture_output=False)
# tag represent a hash in the filament repo that this merge is associated with
def merge_to_main(self, branch, tag, push_to_remote=False):
def merge_to_main(self, branch, push_to_remote=False):
self.update()
assets_dir = self._assets_dir()
# Update commit message
self._git_exec(f'checkout {branch}')
code, old_commit = execute(f'git log --format=%B -n 1', cwd=assets_dir)
if tag and len(tag) > 0:
old_commit += f'\nFILAMENT={tag}'
COMMIT_FILE = '/tmp/golden_commit.txt'
with open(COMMIT_FILE, 'w') as f:
f.write(old_commit)
self._git_exec(f'commit --amend -F {COMMIT_FILE}')
# Do the actual merge
self._git_exec(f'checkout main')
self._git_exec(f'merge --no-ff --no-edit {branch}')
self._git_exec(f'merge --no-ff {branch}')
if push_to_remote and \
(self.access_token_ or self.access_type_ == ACCESS_TYPE_SSH):
self._git_exec(f'push origin main')
@@ -122,7 +109,7 @@ class GoldenManager:
self._git_exec(f'add {GOLDENS_DIR}')
else:
for f in deletes:
self._git_exec(f'rm {os.path.join(GOLDENS_DIR, f)}')
self._git_exec(f'remove {os.path.join(GOLDENS_DIR, f)}')
for f in updates:
shutil.copy2(
os.path.join(src_dir, f),
+2 -26
View File
@@ -13,7 +13,7 @@
# limitations under the License.
import tifffile
import numpy as np
import numpy
def same_image(tiff_file_a, tiff_file_b):
try:
@@ -27,7 +27,7 @@ def same_image(tiff_file_a, tiff_file_b):
return False
# numpy.array_equal() checks if two arrays have the same shape and elements.
if np.array_equal(img1_data, img2_data):
if numpy.array_equal(img1_data, img2_data):
return True
else:
return False
@@ -41,27 +41,3 @@ def same_image(tiff_file_a, tiff_file_b):
except Exception as e:
print(f"An unexpected error occurred: {e}")
return False
def output_image_diff(tiff_file_a, tiff_file_b, output_path):
try:
img1_data = tifffile.imread(tiff_file_a)
img2_data = tifffile.imread(tiff_file_b)
# If the dimensions (height, width, number of channels, number of pages/frames)
# are different, the images are not the same.
if img1_data.shape != img2_data.shape:
raise RuntimeError(f"Images {tiff_file_a} and {tiff_file_b} have different shapes: {img1_data.shape} vs {img2_data.shape}")
diff_img = np.abs(img1_data.astype(np.int16) - img2_data.astype(np.int16))
diff_img = diff_img.astype(np.uint8)
tifffile.imwrite(output_path, diff_img)
except FileNotFoundError:
print(f"Error: One or both files not found ('{file_path1}', '{file_path2}').")
return False
except tifffile.TiffFileError as e:
print(f"Error: One or both files are not valid TIFF files or could not be read. Details: {e}")
return False
except Exception as e:
print(f"An unexpected error occurred: {e}")
return False
+1 -4
View File
@@ -16,9 +16,7 @@
# Sets up the environment for scripts in test/renderdiff/
RENDER_OUTPUT_DIR="$(pwd)/out/renderdiff/renders"
DIFF_OUTPUT_DIR="$(pwd)/out/renderdiff/diffs"
GOLDEN_OUTPUT_DIR="$(pwd)/out/renderdiff/goldens"
OUTPUT_DIR="$(pwd)/out/renderdiff_tests"
RENDERDIFF_TEST_DIR="$(pwd)/test/renderdiff"
MESA_DIR="$(pwd)/mesa/out/"
VENV_DIR="$(pwd)/venv"
@@ -35,7 +33,6 @@ else
fi
function start_() {
mkdir -p ${RENDER_OUTPUT_DIR} ${DIFF_OUTPUT_DIR} ${GOLDEN_OUTPUT_DIR}
if [[ "$GITHUB_WORKFLOW" ]]; then
set -ex
fi
+1 -1
View File
@@ -102,7 +102,7 @@ if __name__ == "__main__":
vk_icd=args.vk_icd)
with open(f'{output_dir}/render_results.json', 'w') as f:
f.write(json.dumps(results, indent=2))
f.write(json.dumps(results))
shutil.copy2(args.test, f'{output_dir}/test.json')
+8 -23
View File
@@ -34,7 +34,7 @@ CONFIG_NEW_SRC_DIR = 'goldens_dir'
CONFIG_GOLDENS_BRANCH = 'goldens_branch'
CONFIG_GOLDENS_UPDATES = 'goldens_updates'
CONFIG_GOLDENS_DELETES = 'goldens_deletes'
CONFIG_PUSH_TO_REMOTE = 'push-to-remote'
CONFIG_AUTO_COMMIT = 'auto-commit'
CONFIG_COMMIT_MSG = 'commit_msg'
def _get_current_branch():
@@ -54,12 +54,12 @@ def _do_update(golden_manager, config):
branch = config[CONFIG_GOLDENS_BRANCH]
src_dir = config[CONFIG_NEW_SRC_DIR]
push_to_remote = config[CONFIG_PUSH_TO_REMOTE]
auto_commit = config[CONFIG_AUTO_COMMIT]
commit_msg = config[CONFIG_COMMIT_MSG]
golden_manager.source_from(src_dir, commit_msg, branch,
updates=updates,
deletes=deletes,
push_to_remote=push_to_remote)
push_to_remote=auto_commit)
def _get_deletes_updates(update_dir, golden_dir):
ret_delete = []
@@ -95,8 +95,7 @@ def _interactive_mode(base_golden_dir):
if code != 0:
print('Failed to generate new goldens')
exit(1)
# Note that this matches RENDER_OUTPUT_DIR in preamble.sh
config[CONFIG_NEW_SRC_DIR] = os.path.join(os.getcwd(), './out/renderdiff/renders')
config[CONFIG_NEW_SRC_DIR] = os.path.join(os.getcwd(), './out/renderdiff_tests/')
else:
def validator(src_dir):
if not os.path.exists(src_dir):
@@ -137,23 +136,16 @@ def _interactive_mode(base_golden_dir):
config[CONFIG_GOLDENS_DELETES] = []
config[CONFIG_GOLDENS_UPDATES] = []
config[CONFIG_PUSH_TO_REMOTE] = \
config[CONFIG_AUTO_COMMIT] = \
prompt_helper(f'Commit golden repo changes to remote?') == PROMPT_YES
return config
if __name__ == "__main__":
parser = ArgParseImpl()
parser.add_argument('--branch', help='Branch of the golden repo to write to')
parser.add_argument('--golden-repo-token', help='Access token for the golden repo')
parser.add_argument('--push-to-remote', action="store_true", help='Access token for the golden repo')
# write-to-branch mode
parser.add_argument('--source', help='Directory containing the new goldens')
parser.add_argument('--commit-msg', help='Message for the commit to the golden repo')
# merge-to-main mode (used in postsubmit)
parser.add_argument('--merge-to-main', action="store_true", help='Merge to main the given branch')
parser.add_argument('--filament-tag', help='Tag to append to the commit message on merge')
parser.add_argument('--golden-repo-token', help='Access token for the golden repo')
args, _ = parser.parse_known_args(sys.argv[1:])
config = {}
@@ -163,24 +155,17 @@ if __name__ == "__main__":
access_token=args.golden_repo_token
)
base_golden_dir = golden_manager.directory()
# This is the write-to-branch mode
if args.branch and args.source and args.commit_msg:
assert os.path.exists(args.source), f'{args.source} (--source) directory not found'
deletes, updates = _get_deletes_updates(args.source, base_golden_dir)
config = {
CONFIG_PUSH_TO_REMOTE: args.push_to_remote,
CONFIG_AUTO_COMMIT: True,
CONFIG_GOLDENS_BRANCH: args.branch,
CONFIG_NEW_SRC_DIR: args.source,
CONFIG_GOLDENS_UPDATES: updates,
CONFIG_GOLDENS_DELETES: deletes,
CONFIG_COMMIT_MSG: args.commit_msg,
}
_do_update(golden_manager, config)
# This is the merge-to-main mode
elif args.branch and args.merge_to_main and args.filament_tag:
golden_manager.merge_to_main(branch=args.branch, tag=args.filament_tag, push_to_remote=True)
# Else, we're in interactive mode of write-to-branch (for local execution).
else:
config = _interactive_mode(base_golden_dir)
_do_update(golden_manager, config)
_do_update(golden_manager, config)
-81
View File
@@ -1,81 +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.
import os
import sys
import flask
import pathlib
import json
from utils import ArgParseImpl
from flask import Flask, request, make_response, send_from_directory
DIR = pathlib.Path(__file__).parent.absolute()
HTML_DIR = os.path.join(DIR, "viewer_html")
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']
del client_config['base_dir']
del client_config['comparison_dir']
del client_config['diff_dir']
@app.route('/r/', methods=['GET'])
def get_r():
return json.dumps(client_config)
@app.route('/g/<path:filepath>', methods=['GET'])
def get_g(filepath):
return send_from_directory(base_dir, filepath)
@app.route('/d/<path:filepath>', methods=['GET'])
def get_d(filepath):
return send_from_directory(diff_dir, filepath)
@app.route('/c/<path:filepath>', methods=['GET'])
def get_c(filepath):
return send_from_directory(comparison_dir, filepath)
@app.route('/<path:filepath>')
def get_static_file(filepath):
return send_from_directory(HTML_DIR, filepath)
@app.route('/')
def get_index():
return send_from_directory(HTML_DIR, 'index.html')
app.url_map.strict_slashes = False
return app
if __name__ == '__main__':
PORT = 8901
parser = ArgParseImpl()
parser.add_argument('--diff', help='Diff directory', required=True)
args, _ = parser.parse_known_args(sys.argv[1:])
with open(os.path.join(args.diff, 'compare_results.json'), 'r') as f:
config = json.loads(f.read())
config['diff_dir'] = os.path.abspath(args.diff)
app = _create_app(config)
from waitress import serve
print(f'Point your browser to http://localhost:{PORT} to see the diff results')
serve(app, host="127.0.0.1", port=PORT)
-299
View File
@@ -1,299 +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.
import { LitElement, html, css } from "https://cdn.jsdelivr.net/gh/lit/dist@3/all/lit-all.min.js";
import './tools.js';
import './tiff-viewer.js';
function getGoldenUrl(testResult) {
return '/g/' + testResult.name;
}
function getCompUrl(testResult) {
return '/c/' + testResult.name;
}
function getDiffUrl(testResult) {
return '/d/' + testResult.diff;
}
const DIFF_VIEW = 'diff'
const GOLDEN_VIEW = 'golden'
const RENDERED_VIEW = 'rendered'
class ExpandedPassedResult extends LitElement {
static get properties() {
return {
content: { type: Object, attribute: 'content' },
};
}
constructor() {
super();
this.content = null;
}
render() {
if (this.content) {
const url = getGoldenUrl(this.content);
return html`<tiff-viewer fileurl="${url}"></tiff-viewer>`;
}
return html``;
}
}
customElements.define('expanded-passed-result', ExpandedPassedResult);
class ExpandedFailedResult extends LitElement {
static get properties() {
return {
content: { type: Object, attribute: 'content' },
leftViewSelected: { type: String },
rightViewSelected: { type: String },
};
}
static styles = css`
.viewer-container {
display: flex;
flex-direction: row;
}
.viewer {
margin: 0 5px;
}
`;
constructor() {
super();
this.content = null;
this.leftViewSelected = GOLDEN_VIEW;
this.rightViewSelected = DIFF_VIEW;
this.addEventListener('radio-change', (ev) => {
if (ev.detail.radioId == 'right') {
this.rightViewSelected = ev.detail.value;
}
if (ev.detail.radioId == 'left') {
this.leftViewSelected = ev.detail.value;
}
});
}
_viewer(name, choices, current) {
const url = {
[GOLDEN_VIEW]: getGoldenUrl,
[RENDERED_VIEW]: getCompUrl,
[DIFF_VIEW]: getDiffUrl,
}[current](this.content);
return html`
<div>
<radio-button-group id="${name}" .choices=${choices} value=${current}></radio-button-group>
<tiff-viewer class="viewer" fileurl="${url}"></tiff-viewer>
</div>
`;
}
render() {
if (!this.content) {
return html``;
}
const v1 = this._viewer("left", [GOLDEN_VIEW, RENDERED_VIEW], this.leftViewSelected);
const v2 = this._viewer("right", [RENDERED_VIEW, DIFF_VIEW], this.rightViewSelected);
return html`
<div class="viewer-container">
${v1}
${v2}
</div>
`;
return html``;
}
}
customElements.define('expanded-failed-result', ExpandedFailedResult);
class App extends LitElement {
static styles = css`
.test-label {
margin-bottom: 10px;
font-size: 12px;
}
.test-item {
display: inline-flex;
flex-direction: column;
border: 1px solid black;
border-radius: 10px;
padding: 10px;
align-items: center;
margin: 5px;
}
.test-item:hover {
box-shadow: 2px 3px 2px 0px rgba(40, 40, 40, .5);
cursor: pointer;
}
.app {
display: flex;
flex-direction: column;
width: 100%;
align-items: center;
padding-top: 20px;
}
.results {
display: inline-flex;
flex-direction: row;
}
.results-container {
display: inline-flex;
flex-direction: column;
border: 1px solid black;
border-radius: 10px;
background: #cbf1c9;
padding: 5px;
align-items: center;
margin: 10px 0;
}
.container-label {
margin-bottom: 10px;
}
.failed-tiffs {
display: inline-flex;
flex-direction: row;
}
.tiff-wrapper {
margin: 0 4px;
display: inline-flex;
flex-direction: column;
align-items: center;
}
.tiff-wrapper-label {
margin: 4px 0;
font-size: 10px;
}
.dialog-header {
font-size: 18px;
margin-bottom: 10px;
}
`;
static properties = {
tests: {type: Array},
dialogContent: {type: Object},
};
async _init() {
const config = await ((await fetch("/r/")).json());
config['results'] = config['results'].sort((a, b) => a.name < b.name ? -1 : (a.name > b.name ? 1 : 0));
this.tests = config['results']
}
constructor() {
super();
this.tests = [];
this.dialogContent = null;
this._init();
this.addEventListener('dialog-closed', () => {
this.dialogContent = null;
});
}
updated(props) {
if (props.has('dialogContent')) {
let dialog = this.shadowRoot.querySelector("#dialog");
dialog.open = !!this.dialogContent;
}
}
_onClick(testDetail) {
this.dialogContent = testDetail;
this.shadowRoot.querySelector("#dialog").open = true;
}
render() {
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>`;
};
const buildTiffs = (ts) => ts.map((t) => {
const printName = t.name.replace('.tif', '');
const tiff = singleTiff(getGoldenUrl(t));
return html`
<div class="test-item" @click="${()=>this._onClick(t)}">
<span class="test-label">${printName}</span>
${tiff}
</div>
`;
});
const buildFailedTiffs = (ts) => ts.map((t) => {
const printName = t.name.replace('.tif', '');
const goldenUrl = getGoldenUrl(t);
const compUrl = getCompUrl(t);
const diffUrl = getDiffUrl(t);
const wrap = (tif, label) => {
return html`
<div class="tiff-wrapper">
${tif}
<div class="tiff-wrapper-label">${label}</div>
</div>`;
}
const tiffs = [
[goldenUrl, 'golden'],
[compUrl, 'rendered'],
[diffUrl, 'diff']
].map((a) => [singleTiff(a[0]), a[1]])
.map((a) => wrap(...a));
return html`
<div class="test-item" @click="${()=>this._onClick(t)}">
<span class="test-label">${printName}</span>
<div class="failed-tiffs">
${tiffs}
</div>
</div>
`;
});
const passedTiffs = buildTiffs(passed);
const failedTiffs = buildFailedTiffs(failed);
let dialogSlot = null;
let dialogHeader = '';
if (this.dialogContent) {
dialogSlot = (() => {
if (this.dialogContent.result == 'ok') {
return html`<expanded-passed-result .content=${this.dialogContent}></expanded-passed-result>`;
}
return html`<expanded-failed-result .content=${this.dialogContent}></expanded-failed-result>`;
})();
dialogHeader = this.dialogContent.name;
}
return html`
<div class="app">
<div class="results-container" style="background:#f5a5a4">
<span class="container-label">Failed</span>
<div class="results">
${failedTiffs}
</div>
</div>
<div class="results-container">
<span class="container-label">Passed</span>
<div class="results">
${passedTiffs}
</div>
</div>
</div>
<modal-dialog id="dialog">
<div class="dialog-header" slot="header">${dialogHeader}</div>
${dialogSlot}
</modal-dialog>
`;
}
}
customElements.define('diff-app', App);
@@ -1,36 +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.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Diff Viewer</title>
<script type="module" src="./app.js"></script>
<script src="https://cdn.jsdelivr.net/npm/utif@3.1.0/UTIF.min.js"></script>
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700" rel="stylesheet">
<style>
html, body {
height: 100%;
margin: 0;
font-family: "Open Sans";
}
</style>
</head>
<body>
<diff-app></diff-app>
</body>
</html>
@@ -1,96 +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.
import { LitElement, html, css } from "https://cdn.jsdelivr.net/gh/lit/dist@3/all/lit-all.min.js";
// Generated by Gemini with some modifications
export class TiffViewer extends LitElement {
static styles = css`
:host {
display: block;
}
canvas {
border: 1px solid #ccc;
width: 100%;
height: 100%;
}
`;
static properties = {
fileurl: {type: String, attribute: 'fileurl'},
};
constructor() {
super();
this.fileurl = null;
}
render() {
return html`<canvas id="tiffCanvas"></canvas>`;
}
updated(props) {
if (props.has('fileurl') && this.fileurl) {
this._updateImage(this.fileurl);
}
}
async _updateImage(fileurl) {
const fileblob = await ((await fetch(this.fileurl)).arrayBuffer());
const canvas = this.shadowRoot.getElementById('tiffCanvas');
const ctx = canvas.getContext('2d');
try {
const arrayBuffer = fileblob;
const ifds = UTIF.decode(arrayBuffer); // Parse TIFF data
if (!ifds || ifds.length === 0) {
console.error('Could not decode TIFF file. No image data found.');
this._clearCanvas();
return;
}
// By default, render the first image in the TIFF (IFD)
const firstImage = ifds[0];
UTIF.decodeImage(arrayBuffer, firstImage, ifds); // Decode the actual pixel data
const rgba = UTIF.toRGBA8(firstImage); // Convert to RGBA
canvas.width = firstImage.width;
canvas.height = firstImage.height;
// The default mode would set alpha to 1 so that RGB differences would be displayed as non-transparent
for (let i = 0; i < firstImage.width; i++) {
for (let j = 0; j < firstImage.height; j++) {
rgba[(j * firstImage.width * 4) + (i * 4) + 3] = 255;
}
}
const imageData = new ImageData(new Uint8ClampedArray(rgba), firstImage.width, firstImage.height);
ctx.putImageData(imageData, 0, 0);
} catch (error) {
console.error('Error processing TIFF file:', error);
this._clearCanvas();
}
}
_clearCanvas() {
const canvas = this.shadowRoot.getElementById('tiffCanvas');
if (canvas) {
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
}
}
customElements.define('tiff-viewer', TiffViewer);
-225
View File
@@ -1,225 +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.
import { LitElement, html, css } from "https://cdn.jsdelivr.net/gh/lit/dist@3/all/lit-all.min.js";
// Generated by Gemini
export class RadioButtonGroup extends LitElement {
static styles = css`
:host {
display: block;
font-family: sans-serif;
}
.radio-group-container {
display: flex;
flex-direction: row;
}
label {
display: flex;
align-items: center;
margin-bottom: 8px;
cursor: pointer;
padding: 8px;
border-radius: 4px;
transition: background-color 0.2s ease-in-out;
}
label:hover {
background-color: #f0f0f0;
}
input[type="radio"] {
margin-right: 8px;
cursor: pointer;
/* Custom radio button appearance */
appearance: none;
-webkit-appearance: none;
width: 18px;
height: 18px;
border: 2px solid #ccc;
border-radius: 50%;
outline: none;
transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}
input[type="radio"]:checked {
border-color: #656565;
background-color: #656565; /* Optional: fill color when checked */
}
input[type="radio"]:checked::before {
content: '';
display: block;
width: 8px;
height: 8px;
margin: 3px; /* Adjust to center the dot */
background-color: white;
border-radius: 50%;
}
input[type="radio"]:focus {
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
.label-text {
font-size: 1rem;
}
`;
static properties = {
/**
* An array of strings representing the choices for the radio buttons.
* @type {Array<string>}
*/
choices: { type: Array },
/**
* The name for the radio button group. This is important for accessibility
* and ensuring only one radio button in the group can be selected.
* @type {string}
*/
name: { type: String },
/**
* The currently selected value.
* @type {string}
*/
value: { type: String, reflect: true },
/**
* The label or title for the radio group.
* @type {string}
*/
groupLabel: { type: String }
};
constructor() {
super();
this.choices = [];
this.name = 'radio-group'; // Default name
this.value = '';
this.groupLabel = '';
}
_handleChange(event) {
const selectedValue = event.target.value;
if (this.value !== selectedValue) {
this.value = selectedValue;
// Dispatch a custom event with the new value
this.dispatchEvent(new CustomEvent('radio-change', {
detail: { value: this.value, radioId: this.id },
bubbles: true, // Allows the event to bubble up through the DOM
composed: true // Allows the event to cross shadow DOM boundaries
}));
}
}
render() {
return html`
<div class="radio-group-container" role="radiogroup" aria-labelledby="group-label">
${this.groupLabel ? html`<span id="group-label" class="group-label">${this.groupLabel}</span>` : ''}
${this.choices.map(choice => html`
<label>
<input
type="radio"
name=${this.name}
.value=${choice}
.checked=${choice === this.value}
@change=${this._handleChange}
>
<span class="label-text">${choice}</span>
</label>
`)}
</div>
`;
}
}
customElements.define('radio-button-group', RadioButtonGroup);
// Generated by Gemini with some modifications
class ModalDialog extends LitElement {
static styles = css`
:host {
display: none; /* Hidden by default */
}
:host([open]) {
display: block; /* Show when open attribute is present */
}
.backdrop {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent black */
display: flex;
justify-content: center;
align-items: center;
z-index: 1000; /* Ensure it's on top */
}
.dialog {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
min-width: 300px; /* Or your desired width */
z-index: 1001; /* Above the backdrop */
display: flex;
flex-direction: column;
align-items: center;
margin: 0 15px;
}
`;
static get properties() {
return {
open: { type: Boolean, reflect: true },
};
}
constructor() {
super();
this.open = false;
}
_handleBackdropClick(event) {
// Close only if the click is directly on the backdrop, not on the dialog itself
if (event.target === this.shadowRoot.querySelector('.backdrop')) {
this.open = false;
this.dispatchEvent(new CustomEvent('dialog-closed', { bubbles: true, composed: true }));
}
}
_handleDialogClick(event) {
// Prevent clicks inside the dialog from bubbling up to the backdrop
event.stopPropagation();
}
render() {
if (!this.open) {
return html``;
}
return html`
<div class="backdrop" @click="${this._handleBackdropClick}">
<div class="dialog" @click="${this._handleDialogClick}">
<slot name="header"><h2>Default Header</h2></slot>
<slot>
<p>This is the default content of the modal.</p>
</slot>
<slot name="footer">
</slot>
</div>
</div>
`;
}
}
customElements.define('modal-dialog', ModalDialog);
@@ -12,24 +12,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import sys
import re
from utils import execute
from utils import execute, ArgParseImpl
RDIFF_UPDATE_GOLDEN_STR = 'RDIFF_BRANCH'
def _parse_commit(commit_str):
lines = commit_str.split('\n')
if len(lines) >= 4:
commit, author, date, _, title, *desc = lines
else:
print(commit_str, file=sys.stderr)
return (
lines[0],
lines[1],
'',
)
def get_last_commit():
res, o = execute('git log -1')
commit, author, date, _, title, *desc = o.split('\n')
commit = commit.split(' ')[1]
title = title.strip()
@@ -43,27 +30,28 @@ def _parse_commit(commit_str):
desc
)
def sanitized_split(line, split_atom='\n'):
return list(
filter(
lambda x: len(x) > 0,
map(
lambda x: x.strip(),
line.split(split_atom)
)
)
)
RDIFF_UPDATE_GOLDEN_STR = 'RDIFF_UPDATE_GOLDEN'
if __name__ == "__main__":
RE_STR = rf"{RDIFF_UPDATE_GOLDEN_STR}(?:S)?=[\[]?([a-zA-Z0-9,\s\-\/]+)[\]]?"
parser = ArgParseImpl()
parser.add_argument('--file', help='A file containing the commit message')
args, _ = parser.parse_known_args(sys.argv[1:])
if not args.file:
msg = sys.stdin.read()
else:
with open(args.file, 'r') as f:
msg = f.read()
RE_STR = f'{RDIFF_UPDATE_GOLDEN_STR}(?:S)?=[\[]?([a-zA-Z0-9,\s]+)[\]]?'
to_update = []
commit, title, description = _parse_commit(msg)
commit, title, description = get_last_commit()
for line in description:
m = re.match(RE_STR, line)
if not m:
continue
print(m.group(1))
exit(0)
# Always default to the main branch
print('main')
to_update += sanitize_split(m.group(1).replace(',', ' '), ' ')
print(','.join(to_update))
+8 -17
View File
@@ -16,22 +16,13 @@
source `dirname $0`/src/preamble.sh
start_
GOLDEN_DIR=$(pwd)/golden_images
if [[ "$GITHUB_WORKFLOW" ]]; then
# The commit message would have been piped as stdin to this script
COMMIT_MSG=$(cat)
GOLDEN_BRANCH=$(echo "${COMMIT_MSG}" | python3 test/renderdiff/src/commit_msg.py)
else
GOLDEN_BRANCH=$(git log -1 | python3 test/renderdiff/src/commit_msg.py)
fi
bash `dirname $0`/generate.sh && \
python3 ${RENDERDIFF_TEST_DIR}/src/golden_manager.py \
--branch=${GOLDEN_BRANCH} \
--output=${GOLDEN_OUTPUT_DIR} && \
start_ && \
bash `dirname $0`/generate.sh && \
mkdir -p ${GOLDEN_DIR} && \
python3 ${RENDERDIFF_TEST_DIR}/src/golden_manager.py --output=${GOLDEN_DIR} && \
python3 ${RENDERDIFF_TEST_DIR}/src/compare.py \
--src=${GOLDEN_OUTPUT_DIR} \
--dest=${RENDER_OUTPUT_DIR} \
--out=${DIFF_OUTPUT_DIR}
end_
--src=${GOLDEN_DIR} \
--dest=${OUTPUT_DIR} && \
end_
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "filament",
"version": "1.61.1",
"version": "1.60.1",
"description": "Real-time physically based rendering engine",
"main": "filament.js",
"module": "filament.js",