From bbea03224d2b9074cd5a1d46cac9c6dc384eefbd Mon Sep 17 00:00:00 2001 From: Matt Hoffman Date: Thu, 26 Jun 2025 14:35:16 -0500 Subject: [PATCH] Comparison with blank skybox. --- CMakeLists.txt | 5 + filament/backend/test/ImageExpectations.cpp | 4 +- test/gltf-comparison/CMakeLists.txt | 32 +++ test/gltf-comparison/expected_images/GLTF.png | Bin 0 -> 2219 bytes .../glTF_cases/Models/Box/LICENSE.md | 15 ++ .../glTF_cases/Models/Box/README.body.md | 7 + .../glTF_cases/Models/Box/README.md | 31 +++ .../glTF_cases/Models/Box/glTF-Binary/Box.glb | Bin 0 -> 1664 bytes .../glTF_cases/Models/Box/glTF-Draco/Box.bin | Bin 0 -> 120 bytes .../glTF_cases/Models/Box/glTF-Draco/Box.gltf | 152 +++++++++++ .../Models/Box/glTF-Embedded/Box.gltf | 142 ++++++++++ .../glTF_cases/Models/Box/glTF/Box.gltf | 142 ++++++++++ .../glTF_cases/Models/Box/glTF/Box0.bin | Bin 0 -> 648 bytes .../glTF_cases/Models/Box/metadata.json | 25 ++ .../Models/Box/screenshot/screenshot-x150.png | Bin 0 -> 5166 bytes .../Models/Box/screenshot/screenshot.png | Bin 0 -> 3276 bytes test/gltf-comparison/glTF_cases/README.md | 3 + test/gltf-comparison/include/GLTFViewer.h | 20 ++ .../include/ImageExpectations.h | 169 ++++++++++++ test/gltf-comparison/src/GLTFViewer.cpp | 17 ++ .../gltf-comparison/src/ImageExpectations.cpp | 248 ++++++++++++++++++ test/gltf-comparison/src/main.cpp | 23 ++ test/gltf-comparison/src/test_CompareGLTF.cpp | 169 ++++++++++++ 23 files changed, 1202 insertions(+), 2 deletions(-) create mode 100644 test/gltf-comparison/CMakeLists.txt create mode 100644 test/gltf-comparison/expected_images/GLTF.png create mode 100644 test/gltf-comparison/glTF_cases/Models/Box/LICENSE.md create mode 100644 test/gltf-comparison/glTF_cases/Models/Box/README.body.md create mode 100644 test/gltf-comparison/glTF_cases/Models/Box/README.md create mode 100644 test/gltf-comparison/glTF_cases/Models/Box/glTF-Binary/Box.glb create mode 100644 test/gltf-comparison/glTF_cases/Models/Box/glTF-Draco/Box.bin create mode 100644 test/gltf-comparison/glTF_cases/Models/Box/glTF-Draco/Box.gltf create mode 100644 test/gltf-comparison/glTF_cases/Models/Box/glTF-Embedded/Box.gltf create mode 100644 test/gltf-comparison/glTF_cases/Models/Box/glTF/Box.gltf create mode 100644 test/gltf-comparison/glTF_cases/Models/Box/glTF/Box0.bin create mode 100644 test/gltf-comparison/glTF_cases/Models/Box/metadata.json create mode 100644 test/gltf-comparison/glTF_cases/Models/Box/screenshot/screenshot-x150.png create mode 100644 test/gltf-comparison/glTF_cases/Models/Box/screenshot/screenshot.png create mode 100644 test/gltf-comparison/glTF_cases/README.md create mode 100644 test/gltf-comparison/include/GLTFViewer.h create mode 100644 test/gltf-comparison/include/ImageExpectations.h create mode 100644 test/gltf-comparison/src/GLTFViewer.cpp create mode 100644 test/gltf-comparison/src/ImageExpectations.cpp create mode 100644 test/gltf-comparison/src/main.cpp create mode 100644 test/gltf-comparison/src/test_CompareGLTF.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 720ff12f90..35321c80c9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -287,6 +287,9 @@ set(FILAMENT ${CMAKE_CURRENT_SOURCE_DIR}) # Where our tools are set(TOOLS ${CMAKE_CURRENT_SOURCE_DIR}/tools) +# Where our tools are +set(TESTS ${CMAKE_CURRENT_SOURCE_DIR}/test) + # ================================================================================================== # Compiler check # ================================================================================================== @@ -896,6 +899,8 @@ if (IS_HOST_PLATFORM) add_subdirectory(${TOOLS}/roughness-prefilter) add_subdirectory(${TOOLS}/specular-color) add_subdirectory(${TOOLS}/uberz) + + add_subdirectory(${TESTS}/gltf-comparison) endif() # Generate exported executables for cross-compiled builds (Android, WebGL, and iOS) diff --git a/filament/backend/test/ImageExpectations.cpp b/filament/backend/test/ImageExpectations.cpp index 5937ac7f92..84305d72d1 100644 --- a/filament/backend/test/ImageExpectations.cpp +++ b/filament/backend/test/ImageExpectations.cpp @@ -21,9 +21,9 @@ #include "utils/Hash.h" #include -#include "BackendTest.h" +//#include "BackendTest.h" #include "backend/PixelBufferDescriptor.h" -#include "private/backend/DriverApi.h" +//#include "private/backend/DriverApi.h" #ifndef FILAMENT_IOS diff --git a/test/gltf-comparison/CMakeLists.txt b/test/gltf-comparison/CMakeLists.txt new file mode 100644 index 0000000000..583b415997 --- /dev/null +++ b/test/gltf-comparison/CMakeLists.txt @@ -0,0 +1,32 @@ +cmake_minimum_required(VERSION 3.19) +project(filament C ASM) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(GLTF_COMPARISON_SOURCES + src/test_CompareGLTF.cpp + src/GLTFViewer.cpp + src/ImageExpectations.cpp + src/main.cpp + ) + +file(COPY glTF_cases DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/glTF_cases) +file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/images/actual_images) +file(COPY expected_images DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/images) + +enable_testing() + +add_executable(gltf_comparison ${GLTF_COMPARISON_SOURCES}) +target_include_directories(gltf_comparison PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) +target_link_libraries(gltf_comparison + gtest + filamentapp + gltfio + absl::str_format +) + + + +include(GoogleTest) +gtest_discover_tests(gltf_comparison) \ No newline at end of file diff --git a/test/gltf-comparison/expected_images/GLTF.png b/test/gltf-comparison/expected_images/GLTF.png new file mode 100644 index 0000000000000000000000000000000000000000..efda3e4b58ee77d96e9ddf0bd1d66f6ab0f6a1db GIT binary patch literal 2219 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&zE~)R&4Yzkn1=x}&cn17q6)!xz)y85lT{ zJY5_^D(1YsX2^Izf#JXgL&+GAgZnj~CCT`Te0eh?dpC!xfhITc08Nzy8f7%9U^E;?)5B=K7%d-0%gNE|V6<9 literal 0 HcmV?d00001 diff --git a/test/gltf-comparison/glTF_cases/Models/Box/LICENSE.md b/test/gltf-comparison/glTF_cases/Models/Box/LICENSE.md new file mode 100644 index 0000000000..84787839a2 --- /dev/null +++ b/test/gltf-comparison/glTF_cases/Models/Box/LICENSE.md @@ -0,0 +1,15 @@ +# LICENSE file for the model: Box + +All files in this directory tree are licensed as indicated below. + +* All files directly associated with the model including all text, image and binary files: + + * [CC BY 4.0 International]("https://creativecommons.org/licenses/by/4.0/legalcode") [SPDX license identifier: "CC-BY-4.0"] + +* This file and all other metadocumentation files including "metadata.json": + + * [Creative Commons Attribtution 4.0 International]("https://creativecommons.org/licenses/by/4.0/legalcode") [SPDX license identifier: "CC-BY-4.0"] + +Full license text of these licenses are available at the links above + +#### Generated by modelmetadata \ No newline at end of file diff --git a/test/gltf-comparison/glTF_cases/Models/Box/README.body.md b/test/gltf-comparison/glTF_cases/Models/Box/README.body.md new file mode 100644 index 0000000000..2e55a1245d --- /dev/null +++ b/test/gltf-comparison/glTF_cases/Models/Box/README.body.md @@ -0,0 +1,7 @@ +## Screenshot + +![screenshot](screenshot/screenshot.png) + +## Description + +Simple cube model. \ No newline at end of file diff --git a/test/gltf-comparison/glTF_cases/Models/Box/README.md b/test/gltf-comparison/glTF_cases/Models/Box/README.md new file mode 100644 index 0000000000..3a19b27978 --- /dev/null +++ b/test/gltf-comparison/glTF_cases/Models/Box/README.md @@ -0,0 +1,31 @@ +# Box + +## Tags + +[core](../../Models-core.md), [testing](../../Models-testing.md) + +## Summary + +One mesh and one material. Start with this. + +## Operations + +* [Display](https://github.khronos.org/glTF-Sample-Viewer-Release/?model=https://raw.GithubUserContent.com/KhronosGroup/glTF-Sample-Assets/main/./Models/Box/glTF-Binary/Box.glb) in SampleViewer +* [Download GLB](https://raw.GithubUserContent.com/KhronosGroup/glTF-Sample-Assets/main/./Models/Box/glTF-Binary/Box.glb) +* [Model Directory](./) + +## Screenshot + +![screenshot](screenshot/screenshot.png) + +## Description + +Simple cube model. + +## Legal + +© 2017, Cesium. [CC BY 4.0 International](https://creativecommons.org/licenses/by/4.0/legalcode) + + - Cesium for Everything + +#### Assembled by modelmetadata \ No newline at end of file diff --git a/test/gltf-comparison/glTF_cases/Models/Box/glTF-Binary/Box.glb b/test/gltf-comparison/glTF_cases/Models/Box/glTF-Binary/Box.glb new file mode 100644 index 0000000000000000000000000000000000000000..95ec886b6b92b134291fd41d34ac9d5349306e0a GIT binary patch literal 1664 zcmb7EOK;jh5S}DW+O$p6v`u^8GeNd_1bm4oEftl43Q#VHgE0$OGB&bJ+Q_oRvHz-n zq(7!Jix*Y|3Dweg=e6_A%bt4u#xVe_&H(fXY|f(@CPIkBiUbn22;I3GyAPRY#|SxE#v~@J z-RZV!7Blr6`_bt&`^`?9nFdzn`eWB2AFOMR#W1rd(&eFRdl`st&r#1>1WTZ{gEyie zT(@AfoJ@Fl@A97_$mlWVoykNr7-KrYd=dEEkNb}c3{ujK0x6e1_PSp7z%Cj)?0>TUa1Jlw h71BAph6{KDmq-`z7OvnOyhpl%4{!}1;Sa&-1*Vq#=uU|`_jVdQ3DV2+RGVq_EuWr=y*>&EmSD9#9koXiZ2Kvh7BfteAg znwgo=fKh;%L1mgSmkJ9|5yO9W5XrC~fg9|(*+7aJ866lIKnB$V@qea;jvNdO08WYz A2><{9 literal 0 HcmV?d00001 diff --git a/test/gltf-comparison/glTF_cases/Models/Box/glTF-Draco/Box.gltf b/test/gltf-comparison/glTF_cases/Models/Box/glTF-Draco/Box.gltf new file mode 100644 index 0000000000..d38bc8c830 --- /dev/null +++ b/test/gltf-comparison/glTF_cases/Models/Box/glTF-Draco/Box.gltf @@ -0,0 +1,152 @@ +{ + "asset": { + "generator": "COLLADA2GLTF", + "version": "2.0" + }, + "scene": 0, + "scenes": [ + { + "nodes": [ + 0 + ] + } + ], + "nodes": [ + { + "children": [ + 1 + ], + "matrix": [ + 1, + 0, + 0, + 0, + 0, + 0, + -1, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 1 + ] + }, + { + "mesh": 0 + } + ], + "meshes": [ + { + "primitives": [ + { + "attributes": { + "NORMAL": 1, + "POSITION": 2 + }, + "indices": 0, + "mode": 4, + "material": 0, + "extensions": { + "KHR_draco_mesh_compression": { + "bufferView": 0, + "attributes": { + "NORMAL": 0, + "POSITION": 1 + } + } + } + } + ], + "name": "Mesh" + } + ], + "accessors": [ + { + "componentType": 5123, + "count": 36, + "max": [ + 23 + ], + "min": [ + 0 + ], + "type": "SCALAR" + }, + { + "componentType": 5126, + "count": 24, + "max": [ + 1.007843137254902, + 1.007843137254902, + 1.007843137254902 + ], + "min": [ + -1.007843137254902, + -1.007843137254902, + -1.007843137254902 + ], + "type": "VEC3" + }, + { + "componentType": 5126, + "count": 24, + "max": [ + 0.5004885197850513, + 0.5004885197850513, + 0.5004885197850513 + ], + "min": [ + -0.5004885197850513, + -0.5004885197850513, + -0.5004885197850513 + ], + "type": "VEC3" + } + ], + "materials": [ + { + "pbrMetallicRoughness": { + "baseColorFactor": [ + 0.800000011920929, + 0, + 0, + 1 + ], + "metallicFactor": 0, + "roughnessFactor": 1 + }, + "name": "Red", + "emissiveFactor": [ + 0, + 0, + 0 + ], + "alphaMode": "OPAQUE", + "doubleSided": false + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteOffset": 0, + "byteLength": 118 + } + ], + "buffers": [ + { + "name": "Box", + "byteLength": 120, + "uri": "Box.bin" + } + ], + "extensionsRequired": [ + "KHR_draco_mesh_compression" + ], + "extensionsUsed": [ + "KHR_draco_mesh_compression" + ] +} diff --git a/test/gltf-comparison/glTF_cases/Models/Box/glTF-Embedded/Box.gltf b/test/gltf-comparison/glTF_cases/Models/Box/glTF-Embedded/Box.gltf new file mode 100644 index 0000000000..ea0e8ada69 --- /dev/null +++ b/test/gltf-comparison/glTF_cases/Models/Box/glTF-Embedded/Box.gltf @@ -0,0 +1,142 @@ +{ + "asset": { + "generator": "COLLADA2GLTF", + "version": "2.0" + }, + "scene": 0, + "scenes": [ + { + "nodes": [ + 0 + ] + } + ], + "nodes": [ + { + "children": [ + 1 + ], + "matrix": [ + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0 + ] + }, + { + "mesh": 0 + } + ], + "meshes": [ + { + "primitives": [ + { + "attributes": { + "NORMAL": 1, + "POSITION": 2 + }, + "indices": 0, + "mode": 4, + "material": 0 + } + ], + "name": "Mesh" + } + ], + "accessors": [ + { + "bufferView": 0, + "byteOffset": 0, + "componentType": 5123, + "count": 36, + "max": [ + 23 + ], + "min": [ + 0 + ], + "type": "SCALAR" + }, + { + "bufferView": 1, + "byteOffset": 0, + "componentType": 5126, + "count": 24, + "max": [ + 1.0, + 1.0, + 1.0 + ], + "min": [ + -1.0, + -1.0, + -1.0 + ], + "type": "VEC3" + }, + { + "bufferView": 1, + "byteOffset": 288, + "componentType": 5126, + "count": 24, + "max": [ + 0.5, + 0.5, + 0.5 + ], + "min": [ + -0.5, + -0.5, + -0.5 + ], + "type": "VEC3" + } + ], + "materials": [ + { + "pbrMetallicRoughness": { + "baseColorFactor": [ + 0.800000011920929, + 0.0, + 0.0, + 1.0 + ], + "metallicFactor": 0.0 + }, + "name": "Red" + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteOffset": 576, + "byteLength": 72, + "target": 34963 + }, + { + "buffer": 0, + "byteOffset": 0, + "byteLength": 576, + "byteStride": 12, + "target": 34962 + } + ], + "buffers": [ + { + "byteLength": 648, + "uri": "data:application/octet-stream;base64,AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAvwAAAL8AAAA/AAAAPwAAAL8AAAA/AAAAvwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAPwAAAL8AAAA/AAAAvwAAAL8AAAA/AAAAPwAAAL8AAAC/AAAAvwAAAL8AAAC/AAAAPwAAAD8AAAA/AAAAPwAAAL8AAAA/AAAAPwAAAD8AAAC/AAAAPwAAAL8AAAC/AAAAvwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAvwAAAD8AAAC/AAAAPwAAAD8AAAC/AAAAvwAAAL8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAC/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAC/AAAAPwAAAL8AAAC/AAAAPwAAAD8AAAC/AAABAAIAAwACAAEABAAFAAYABwAGAAUACAAJAAoACwAKAAkADAANAA4ADwAOAA0AEAARABIAEwASABEAFAAVABYAFwAWABUA" + } + ] +} diff --git a/test/gltf-comparison/glTF_cases/Models/Box/glTF/Box.gltf b/test/gltf-comparison/glTF_cases/Models/Box/glTF/Box.gltf new file mode 100644 index 0000000000..7f603f07fc --- /dev/null +++ b/test/gltf-comparison/glTF_cases/Models/Box/glTF/Box.gltf @@ -0,0 +1,142 @@ +{ + "asset": { + "generator": "COLLADA2GLTF", + "version": "2.0" + }, + "scene": 0, + "scenes": [ + { + "nodes": [ + 0 + ] + } + ], + "nodes": [ + { + "children": [ + 1 + ], + "matrix": [ + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0 + ] + }, + { + "mesh": 0 + } + ], + "meshes": [ + { + "primitives": [ + { + "attributes": { + "NORMAL": 1, + "POSITION": 2 + }, + "indices": 0, + "mode": 4, + "material": 0 + } + ], + "name": "Mesh" + } + ], + "accessors": [ + { + "bufferView": 0, + "byteOffset": 0, + "componentType": 5123, + "count": 36, + "max": [ + 23 + ], + "min": [ + 0 + ], + "type": "SCALAR" + }, + { + "bufferView": 1, + "byteOffset": 0, + "componentType": 5126, + "count": 24, + "max": [ + 1.0, + 1.0, + 1.0 + ], + "min": [ + -1.0, + -1.0, + -1.0 + ], + "type": "VEC3" + }, + { + "bufferView": 1, + "byteOffset": 288, + "componentType": 5126, + "count": 24, + "max": [ + 0.5, + 0.5, + 0.5 + ], + "min": [ + -0.5, + -0.5, + -0.5 + ], + "type": "VEC3" + } + ], + "materials": [ + { + "pbrMetallicRoughness": { + "baseColorFactor": [ + 0.800000011920929, + 0.0, + 0.0, + 1.0 + ], + "metallicFactor": 0.0 + }, + "name": "Red" + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteOffset": 576, + "byteLength": 72, + "target": 34963 + }, + { + "buffer": 0, + "byteOffset": 0, + "byteLength": 576, + "byteStride": 12, + "target": 34962 + } + ], + "buffers": [ + { + "byteLength": 648, + "uri": "Box0.bin" + } + ] +} diff --git a/test/gltf-comparison/glTF_cases/Models/Box/glTF/Box0.bin b/test/gltf-comparison/glTF_cases/Models/Box/glTF/Box0.bin new file mode 100644 index 0000000000000000000000000000000000000000..d7798abb5161eca17ac8a6883a1f53c6786b484d GIT binary patch literal 648 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 AL;wH) literal 0 HcmV?d00001 diff --git a/test/gltf-comparison/glTF_cases/Models/Box/screenshot/screenshot.png b/test/gltf-comparison/glTF_cases/Models/Box/screenshot/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..b74e7858577cca20f41d846a527b808e0cbc6d83 GIT binary patch literal 3276 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 +#include +#include + +#include "gtest/gtest.h" + +// Arguments are (Renderer* renderer, ImageExpectations& expectations, +// ScreenshotParams screenshotParams) +#define EXPECT_IMAGE(renderer, expectations, screenshotParams) \ +do { \ + expectations.addExpectation( \ + __FILE__, \ + __LINE__, \ + renderer, \ + screenshotParams); \ +} while (0) + +namespace test { + +/** + * Stores user-provided configuration values for an image expectation + */ +class ScreenshotParams { +public: + // TODO(b/422804941): Add a set of environments where this test should use a different golden. + ScreenshotParams(int width, int height, std::string fileName, + bool isSrgb = false); + + int width() const; + int height() const; + bool isSrgb() const; + + static std::string actualDirectoryPath(); + std::string actualFileName() const; + std::string actualFilePath() const; + static std::string expectedDirectoryPath(); + std::string expectedFileName() const; + std::string expectedFilePath() const; + const std::string filePrefix() const; + +private: + int mWidth; + int mHeight; + bool mIsSrgb; + std::string mFileName; +}; + +/** + * When created adds a command to the GPU pipeline to copy the render target into a buffer and + * stores the result. + * If this object is destroyed before the GPU pipeline is flushed it will leak memory in order to + * avoid the GPU pipeline callback being a use-after-free. + */ +class RenderTargetDump { +public: + using ReadPixels = std::function; + static ReadPixels readFromRenderer(filament::Renderer* renderer); + + RenderTargetDump(const ReadPixels& readPixels, const ScreenshotParams& params); + RenderTargetDump(RenderTargetDump&& other) = default; + RenderTargetDump& operator=(RenderTargetDump&& other) = default; + ~RenderTargetDump(); + + /** + * Should only bue used if BytesFilled returns true. + * @return The hash of the stored bytes. + */ + uint32_t hash() const; + /** + * Gets the bytes of the render target. The hash should usually be preferable for comparisons + * but this is available for debugging. + * @return The stored bytes. + */ + const std::vector& bytes() const; + /** + * Thread safe as this is backed by an atomic. + * Once this returns true it will never return false. + * @return Whether the bytes have actually been copied from the GPU to the buffer. + */ + bool bytesFilled() const; + +private: + struct Internal { + explicit Internal(const ScreenshotParams& params); + ScreenshotParams params; + std::atomic bytesFilled = false; + std::vector bytes; + + uint32_t hash() const; + }; + + // We need a memory location that won't be invalidated to pass to GPU callbacks as they can't + // be canceled during the destructor. + std::unique_ptr mInternal; +}; + +class LoadedPng { +public: + explicit LoadedPng(std::string filePath); + + uint32_t hash() const; + + const std::vector& bytes() const; + +private: + std::string mFilePath; + std::vector mBytes; +}; + +class ImageExpectation { +public: + ImageExpectation(const char* fileName, int lineNumber, RenderTargetDump::ReadPixels readPixels, + ScreenshotParams params); + + void evaluate(); + +private: + void compareImage() const; + + bool mEvaluated = false; + const char* mFileName; + int mLineNumber; + ScreenshotParams mParams; + RenderTargetDump mResult; +}; + +class ImageExpectations { +public: + explicit ImageExpectations() = default; + ~ImageExpectations(); + + /** + * Not meant to be called directly, use EXPECT_IMAGE to get the file name and line number + */ + void addExpectation(const char* fileName, int lineNumber, + filament::Renderer* renderer, ScreenshotParams params); + + void evaluate(); + + static void markImageAsFailure(const std::string& imagePrefix); + +private: + // Store expectations in unique pointers because they are self referential. + std::vector> mExpectations; +}; + +} // namespace test + +#endif //TNT_IMAGE_EXPECTATIONS_H diff --git a/test/gltf-comparison/src/GLTFViewer.cpp b/test/gltf-comparison/src/GLTFViewer.cpp new file mode 100644 index 0000000000..87f8c151b8 --- /dev/null +++ b/test/gltf-comparison/src/GLTFViewer.cpp @@ -0,0 +1,17 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "GLTFViewer.h" diff --git a/test/gltf-comparison/src/ImageExpectations.cpp b/test/gltf-comparison/src/ImageExpectations.cpp new file mode 100644 index 0000000000..f5ec2d82cb --- /dev/null +++ b/test/gltf-comparison/src/ImageExpectations.cpp @@ -0,0 +1,248 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ImageExpectations.h" + +#include "gmock/gmock.h" +#include "absl/strings/str_format.h" +#include "utils/Hash.h" +#include + +#include +#include +#include + +namespace test { + +ScreenshotParams::ScreenshotParams(int width, int height, std::string fileName, bool isSrgb) + : mWidth(width), + mHeight(height), + mIsSrgb(isSrgb), + mFileName(std::move(fileName)) {} + +int ScreenshotParams::width() const { + return mWidth; +} + +int ScreenshotParams::height() const { + return mHeight; +} + +bool ScreenshotParams::isSrgb() const { + return mIsSrgb; +} + +std::string ScreenshotParams::actualDirectoryPath() { + return "images/actual_images"; +} + +std::string ScreenshotParams::actualFileName() const { + return absl::StrFormat("%s_actual.png", mFileName); +} + +std::string ScreenshotParams::actualFilePath() const { + return absl::StrFormat("%s/%s", actualDirectoryPath(), actualFileName()); +} + +std::string ScreenshotParams::expectedDirectoryPath() { + return "images/expected_images"; +} + +std::string ScreenshotParams::expectedFileName() const { + return absl::StrFormat("%s.png", mFileName); +} + +std::string ScreenshotParams::expectedFilePath() const { + return absl::StrFormat("%s/%s", expectedDirectoryPath(), expectedFileName()); +} + +const std::string ScreenshotParams::filePrefix() const { + // TODO(b/422804941): If there are platform specific goldens, when on those platforms append a + // unique platform identifying string to this. + return mFileName; +} + +ImageExpectation::ImageExpectation(const char* fileName, int lineNumber, + RenderTargetDump::ReadPixels readPixels, ScreenshotParams params) + : mFileName(fileName), + mLineNumber(lineNumber), + mParams(std::move(params)), + mResult(readPixels, mParams) {} + +void ImageExpectation::evaluate() { + // Ensure this is only evaluated once. + if (mEvaluated) { + return; + } + mEvaluated = true; + + // Do the actual image comparison inside a scoped trace with the stored file and line. + { + testing::ScopedTrace trace(mFileName, mLineNumber, ""); + compareImage(); + } +} + +void ImageExpectation::compareImage() const { + bool bytesFilled = mResult.bytesFilled(); + // If this fails, it likely means that BackendTest::flushAndWait needs to be called before + // ImageExpectations is evaluated or destroyed. + EXPECT_THAT(bytesFilled, testing::IsTrue()) + << "Render target wasn't copied to the buffer for " << mFileName; + if (bytesFilled) { + // Rather than directly compare the two images compare their hashes because comparing very + // large arrays generates way too much debug output to be useful. + uint32_t actualHash = mResult.hash(); +#ifndef FILAMENT_IOS + LoadedPng loadedImage(mParams.expectedFilePath()); + uint32_t loadedImageHash = loadedImage.hash(); + auto compareToImageMatcher = testing::Eq(loadedImageHash); + if (!testing::Matches(compareToImageMatcher)(actualHash)) { + ImageExpectations::markImageAsFailure(mParams.filePrefix()); + } + EXPECT_THAT(actualHash, compareToImageMatcher) << mParams.expectedFileName(); +#endif + } +} + +ImageExpectations::~ImageExpectations() { + // Guarantee that all expectations are evaluated when this leaves scope even if the caller + // forgot to manually evaluate them. + evaluate(); +} + +void ImageExpectations::addExpectation(const char* fileName, int lineNumber, + filament::Renderer* renderer, ScreenshotParams params) { + mExpectations.emplace_back(std::make_unique( + fileName, lineNumber, + [renderer](uint32_t x, uint32_t y, uint32_t width, uint32_t height, + filament::backend::PixelBufferDescriptor&& pb) { + renderer->readPixels(x, y, width, height, std::move(pb)); + }, + std::move(params))); +} + +void ImageExpectations::evaluate() { + for (auto& expectation: mExpectations) { + expectation->evaluate(); + } + mExpectations.clear(); +} + +void ImageExpectations::markImageAsFailure(const std::string& imagePrefix) { + +} + +RenderTargetDump::RenderTargetDump(const RenderTargetDump::ReadPixels& readPixels, + const ScreenshotParams& params) + : mInternal(std::make_unique(params)) { + const size_t size = mInternal->params.width() * mInternal->params.height() * 4; + mInternal->bytes.resize(size); + + auto cb = [](void* buffer, size_t size, void* user) { + auto* internal = static_cast(user); + internal->bytesFilled = true; +#ifndef FILAMENT_IOS + image::LinearImage image; + if (internal->params.isSrgb()) { + image = image::toLinearWithAlpha(internal->params.width(), + internal->params.height(), + internal->params.width() * 4, (uint8_t*)buffer); + } else { + // The image data is already linear, so pass in transforms that simply go from uint8_t + // to float. toLinearWithAlpha divides the float values by uint8_t max so there's no + // need to scale it to [0, 1] + image = image::toLinearWithAlpha( + internal->params.width(), internal->params.height(), + internal->params.width() * 4, (uint8_t*) buffer, + [](uint8_t value) -> float { return value; }, + [](filament::math::float4 rgba) -> filament::math::float4 { return rgba; }); + } + std::string filePath = internal->params.actualFilePath(); + std::ofstream pngStream(filePath, std::ios::binary | std::ios::trunc); + // To avoid going from linear -> sRGB -> linear save the PNG as linear. + image::ImageEncoder::encode(pngStream, image::ImageEncoder::Format::PNG_LINEAR, image, "", + filePath); +#endif + }; + filament::backend::PixelBufferDescriptor pb(mInternal->bytes.data(), size, + filament::backend::PixelDataFormat::RGBA, filament::backend::PixelDataType::UBYTE, cb, + (void*)mInternal.get()); + readPixels(0, 0, mInternal->params.width(), mInternal->params.height(), std::move(pb)); +} + +RenderTargetDump::~RenderTargetDump() { + // If the GPU callback hasn't been made yet then there's a callback elsewhere that has a copy of + // the internal pointer. But there's no guarantee that the callback will be ever made if the GPU + // pipeline wasn't run for some reason. So it is necessary to leak the memory. + // It would be possible to try to coordinate with the callback to have it clean up the memory, + // but if this condition happens there's already an issue with the test case so there's no need. + if (!bytesFilled()) { + mInternal.release(); + } +} + +uint32_t RenderTargetDump::Internal::hash() const { + return utils::hash::murmur3((uint32_t*)bytes.data(), bytes.size() / 4, 0); +} + +uint32_t RenderTargetDump::hash() const { + return mInternal->hash(); +} + +const std::vector& RenderTargetDump::bytes() const { + return mInternal->bytes; +} + +bool RenderTargetDump::bytesFilled() const { + return mInternal->bytesFilled; +} + +RenderTargetDump::Internal::Internal(const ScreenshotParams& params) : params(params) {} + +LoadedPng::LoadedPng(std::string filePath) : mFilePath(std::move(filePath)) { +#ifndef FILAMENT_IOS + std::ifstream pngStream(mFilePath, std::ios::binary); + image::LinearImage loadedImage = image::ImageDecoder::decode(pngStream, filePath, + image::ImageDecoder::ColorSpace::LINEAR); + size_t valuesInImage = loadedImage.getWidth() * loadedImage.getHeight() * + loadedImage.getChannels(); + // The linear image is loaded with each component as [0.0, 1.0] but should be [0, 255], so + // convert them. + mBytes = std::vector(valuesInImage); + for (int i = 0; i < valuesInImage; ++i) { + mBytes[i] = static_cast(loadedImage.get()[i] * 255.0f); + } +#endif + // For platforms that don't support the image loading library, leave the loaded data blank. +} + +uint32_t LoadedPng::hash() const { + EXPECT_THAT(mBytes, testing::Not(testing::IsEmpty())) + << "Failed to load expected test result: " << mFilePath << ".\n" + << "Did you forget to sync CMake after updating the expected image in the source " + "directory?"; + if (mBytes.empty()) { + return 0; + } + return utils::hash::murmur3((uint32_t*)mBytes.data(), mBytes.size() / 4, 0); +} + +const std::vector& LoadedPng::bytes() const { + return mBytes; +} + +} // namespace test diff --git a/test/gltf-comparison/src/main.cpp b/test/gltf-comparison/src/main.cpp new file mode 100644 index 0000000000..412e38a151 --- /dev/null +++ b/test/gltf-comparison/src/main.cpp @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +int main(int argc, char** argv) { + testing::InitGoogleTest(&argc, argv); + + return RUN_ALL_TESTS(); +} \ No newline at end of file diff --git a/test/gltf-comparison/src/test_CompareGLTF.cpp b/test/gltf-comparison/src/test_CompareGLTF.cpp new file mode 100644 index 0000000000..558e93a645 --- /dev/null +++ b/test/gltf-comparison/src/test_CompareGLTF.cpp @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "ImageExpectations.h" + +using namespace filament; +using utils::Entity; +using utils::EntityManager; + +struct App { + Config config; + VertexBuffer* vb; + IndexBuffer* ib; + Material* mat; + Camera* cam; + Entity camera; + Skybox* skybox; + Entity renderable; +}; + +struct Vertex { + filament::math::float2 position; + uint32_t color; +}; + +static const Vertex TRIANGLE_VERTICES[3] = { + {{1, 0}, 0xffff0000u}, + {{cos(M_PI * 2 / 3), sin(M_PI * 2 / 3)}, 0xff00ff00u}, + {{cos(M_PI * 4 / 3), sin(M_PI * 4 / 3)}, 0xff0000ffu}, +}; + +static constexpr uint16_t TRIANGLE_INDICES[3] = { 0, 1, 2 }; + +const std::string material = R"(material { + name : BakedColor, + requires : [ + color + ], + shadingModel : unlit, + culling : none, + featureLevel : 0 +} + +fragment { + void material(inout MaterialInputs material) { + prepareMaterial(material); + material.baseColor = getColor(); + } +} +)"; + +class CompareGLTFTest : public testing::Test { +public: + test::ImageExpectations mExpectations; + + void postRender(Engine*, View* view, Scene*, Renderer* renderer); +}; + +void CompareGLTFTest::postRender(Engine*, View* view, Scene*, Renderer* renderer) { + EXPECT_IMAGE(renderer, mExpectations, test::ScreenshotParams(512, 512, "GLTF")); + + FilamentApp::get().close(); +} + +TEST_F(CompareGLTFTest, Compare) { + App app; + + auto setup = [&app](Engine* engine, View* view, Scene* scene) { + app.skybox = Skybox::Builder().color({0.1, 0.125, 0.25, 1.0}).build(*engine); + scene->setSkybox(app.skybox); + view->setPostProcessingEnabled(false); + static_assert(sizeof(Vertex) == 12, "Strange vertex size."); + /*app.vb = VertexBuffer::Builder() + .vertexCount(3) + .bufferCount(1) + .attribute(VertexAttribute::POSITION, 0, VertexBuffer::AttributeType::FLOAT2, 0, 12) + .attribute(VertexAttribute::COLOR, 0, VertexBuffer::AttributeType::UBYTE4, 8, 12) + .normalized(VertexAttribute::COLOR) + .build(*engine); + app.vb->setBufferAt(*engine, 0, + VertexBuffer::BufferDescriptor(TRIANGLE_VERTICES, 36, nullptr)); + app.ib = IndexBuffer::Builder() + .indexCount(3) + .bufferType(IndexBuffer::IndexType::USHORT) + .build(*engine); + app.ib->setBuffer(*engine, + IndexBuffer::BufferDescriptor(TRIANGLE_INDICES, 6, nullptr)); + app.mat = Material::Builder() + .package(material.c_str(), material.size()) + .build(*engine); + app.renderable = EntityManager::get().create(); + RenderableManager::Builder(1) + .boundingBox({{ -1, -1, -1 }, { 1, 1, 1 }}) + .material(0, app.mat->getDefaultInstance()) + .geometry(0, RenderableManager::PrimitiveType::TRIANGLES, app.vb, app.ib, 0, 3) + .culling(false) + .receiveShadows(false) + .castShadows(false) + .build(*engine, app.renderable); + scene->addEntity(app.renderable);*/ + app.camera = utils::EntityManager::get().create(); + app.cam = engine->createCamera(app.camera); + view->setCamera(app.cam); + }; + + auto cleanup = [&app](Engine* engine, View*, Scene*) { + engine->destroy(app.skybox); + //engine->destroy(app.renderable); + //engine->destroy(app.mat); + //engine->destroy(app.vb); + //engine->destroy(app.ib); + engine->destroyCameraComponent(app.camera); + utils::EntityManager::get().destroy(app.camera); + }; + + FilamentApp::get().animate([&app](Engine* engine, View* view, double now) { + constexpr float ZOOM = 1.5f; + const uint32_t w = view->getViewport().width; + const uint32_t h = view->getViewport().height; + const float aspect = (float) w / h; + app.cam->setProjection(Camera::Projection::ORTHO, + -aspect * ZOOM, aspect * ZOOM, + -ZOOM, ZOOM, 0, 1); + auto& tcm = engine->getTransformManager(); + tcm.setTransform(tcm.getInstance(app.renderable), + filament::math::mat4f::rotation(now, filament::math::float3{ 0, 0, 1 })); + }); + + app.config.backend = filament::Engine::Backend::METAL; + + FilamentApp::get().run(app.config, setup, cleanup, FilamentApp::ImGuiCallback(), + FilamentApp::PreRenderCallback(), + [this](Engine* engine, View* view, Scene* scene, Renderer* renderer) { + postRender(engine, view, scene, renderer); + }); +} \ No newline at end of file