From ef3c39197888cba7ec71c9045c2c8bdf6d4eb8be Mon Sep 17 00:00:00 2001 From: Doris Wu Date: Fri, 24 Oct 2025 11:04:00 +0800 Subject: [PATCH] buffer update opt: Add material instances stress test sample (#9345) --- samples/CMakeLists.txt | 2 + samples/materialinstancestress.cpp | 307 +++++++++++++++++++++++++++++ 2 files changed, 309 insertions(+) create mode 100644 samples/materialinstancestress.cpp diff --git a/samples/CMakeLists.txt b/samples/CMakeLists.txt index f2889e7262..f673898ed3 100644 --- a/samples/CMakeLists.txt +++ b/samples/CMakeLists.txt @@ -281,6 +281,7 @@ if (NOT ANDROID) add_demo(image_viewer) add_demo(lightbulb) add_demo(material_sandbox) + add_demo(materialinstancestress) add_demo(multiple_windows) # Tint does not support setting gl_PointSize, disable the relevant sample if using WebGPU # https://github.com/gpuweb/gpuweb/issues/1190 @@ -306,6 +307,7 @@ if (NOT ANDROID) target_link_libraries(hellopbr PRIVATE filameshio suzanne-resources) target_link_libraries(hellostereo PRIVATE filameshio suzanne-resources) target_link_libraries(image_viewer PRIVATE viewer imageio) + target_link_libraries(materialinstancestress PRIVATE geometry filameshio suzanne-resources) target_link_libraries(multiple_windows PRIVATE filameshio suzanne-resources) target_link_libraries(rendertarget PRIVATE filameshio suzanne-resources) target_link_libraries(sample_cloth PRIVATE filameshio) diff --git a/samples/materialinstancestress.cpp b/samples/materialinstancestress.cpp new file mode 100644 index 0000000000..b941350eb4 --- /dev/null +++ b/samples/materialinstancestress.cpp @@ -0,0 +1,307 @@ +/* + * 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 "common/arguments.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include + +#include + +#include + +#include +#include +#include +#include + +#include "generated/resources/monkey.h" +#include "generated/resources/resources.h" + +using namespace filament; +using namespace filament::math; +using namespace utils; +using namespace filamesh; + +struct App { + struct UiState { + int objectCountSlider = 100; + float updateRatio = 0.5f; + bool animate = true; + int deltaCount = 5; + } ui; + + int currentObjectCount = 0; + int desiredObjectCount = 1000; + uint32_t frameCounter = 0; + float spacing = 2.5f; + float distanceToCamera = -20.0f; + int gridDim = 1; + + Material* litMaterial = nullptr; + MeshReader::Mesh suzanneTemplate; + Entity light; + + std::vector materialInstances; + std::vector renderables; +}; + +static void printUsage(char* name) { + std::string exec_name(Path(name).getName()); + std::string usage( + "MATERIALINSTANCESTRESS renders a lot of suzanne models and changes their material " + "properties periodically.\n" + "Usage:\n" + " MATERIALINSTANCESTRESS [options]\n" + "Options:\n" + " --help, -h\n" + " Prints this message\n\n" + "API_USAGE"); + const std::string from("MATERIALINSTANCESTRESS"); + for (size_t pos = usage.find(from); pos != std::string::npos; pos = usage.find(from, pos)) { + usage.replace(pos, from.length(), exec_name); + } + const std::string apiUsage("API_USAGE"); + for (size_t pos = usage.find(apiUsage); pos != std::string::npos; pos = usage.find(apiUsage, pos)) { + usage.replace(pos, apiUsage.length(), samples::getBackendAPIArgumentsUsage()); + } + std::cout << usage; +} + +static int handleCommandLineArguments(int argc, char* argv[], Config* config) { + static constexpr const char* OPTSTR = "ha:"; + static const struct option OPTIONS[] = { + { "help", no_argument, nullptr, 'h' }, + { "api", required_argument, nullptr, 'a' }, + { nullptr, 0, nullptr, 0 } + }; + int opt; + int option_index = 0; + while ((opt = getopt_long(argc, argv, OPTSTR, OPTIONS, &option_index)) >= 0) { + std::string arg(optarg ? optarg : ""); + switch (opt) { + default: + case 'h': + printUsage(argv[0]); + exit(0); + case 'a': + config->backend = samples::parseArgumentsForBackend(arg); + break; + } + } + return optind; +} + +static void removeObjects(Engine* engine, Scene* scene, App& app, int count) { + if (count <= 0) return; + + EntityManager& em = EntityManager::get(); + const int removeCount = std::min(count, app.currentObjectCount); + + for (int i = 0; i < removeCount; ++i) { + Entity renderable = app.renderables.back(); + scene->remove(renderable); + engine->destroy(renderable); + em.destroy(renderable); + app.renderables.pop_back(); + + MaterialInstance* mi = app.materialInstances.back(); + engine->destroy(mi); + app.materialInstances.pop_back(); + } + + app.currentObjectCount = app.renderables.size(); +} + +static void clearScene(Engine* engine, Scene* scene, App& app) { + removeObjects(engine, scene, app, app.currentObjectCount); +} + +static void addObjects(Engine* engine, Scene* scene, App& app, int count) { + if (count <= 0) return; + + TransformManager& tcm = engine->getTransformManager(); + EntityManager& em = EntityManager::get(); + + int const oldTotal = app.currentObjectCount; + int const newTotal = oldTotal + count; + + app.renderables.reserve(newTotal); + app.materialInstances.reserve(newTotal); + + for (int i = oldTotal; i < newTotal; ++i) { + int const x = i % app.gridDim; + int const y = i / app.gridDim; + + float3 const position = { + (x - app.gridDim / 2.0f) * app.spacing, + (y - app.gridDim / 2.0f) * app.spacing, + app.distanceToCamera + }; + + MaterialInstance* mi = app.litMaterial->createInstance(); + mi->setParameter("baseColor", RgbType::sRGB, float3{1.0f}); + mi->setParameter("metallic", 0.0f); + mi->setParameter("roughness", 0.4f); + app.materialInstances.push_back(mi); + + Entity renderable = em.create(); + RenderableManager::Builder(1) + .boundingBox({{-1, -1, -1}, {1, 1, 1}}) + .material(0, mi) + .geometry(0, RenderableManager::PrimitiveType::TRIANGLES, + app.suzanneTemplate.vertexBuffer, app.suzanneTemplate.indexBuffer) + .culling(true) + .castShadows(false) + .build(*engine, renderable); + + scene->addEntity(renderable); + app.renderables.push_back(renderable); + + tcm.setTransform(tcm.getInstance(renderable), mat4f::translation(position)); + } + + app.currentObjectCount = newTotal; +} + +static void createSceneObjects(Engine* engine, Scene* scene, App& app) { + app.gridDim = static_cast(ceil(sqrt(app.desiredObjectCount))); + addObjects(engine, scene, app, std::max(0, app.desiredObjectCount - app.currentObjectCount)); +} + +int main(int argc, char** argv) { + Config config; + config.title = "Material Instances Stress Test"; + config.iblDirectory = FilamentApp::getRootAssetsPath() + "assets/ibl/lightroom_14b"; + handleCommandLineArguments(argc, argv, &config); + + App app; + + auto setup = [&app](Engine* engine, View* view, Scene* scene) { + app.desiredObjectCount = app.ui.objectCountSlider; + app.suzanneTemplate = MeshReader::loadMeshFromBuffer(engine, + MONKEY_SUZANNE_DATA, nullptr, nullptr, nullptr); + + app.litMaterial = Material::Builder() + .package(RESOURCES_AIDEFAULTMAT_DATA, RESOURCES_AIDEFAULTMAT_SIZE) + .build(*engine); + + app.light = EntityManager::get().create(); + LightManager::Builder(LightManager::Type::SUN) + .color(Color::toLinear(sRGBColor(0.98f, 0.92f, 0.89f))) + .intensity(110000) + .direction({0.7, -1, -0.8}) + .sunAngularRadius(1.9f) + .castShadows(false) + .build(*engine, app.light); + scene->addEntity(app.light); + }; + + auto cleanup = [&app](Engine* engine, View* view, Scene* scene) { + clearScene(engine, scene, app); + + engine->destroy(app.light); + EntityManager& em = EntityManager::get(); + em.destroy(app.light); + + engine->destroy(app.litMaterial); + engine->destroy(app.suzanneTemplate.renderable); + engine->destroy(app.suzanneTemplate.vertexBuffer); + engine->destroy(app.suzanneTemplate.indexBuffer); + }; + + auto gui = [&app](Engine* engine, View* view) { + ImGui::Begin("Material Instance Stress Test Controls"); + ImGui::Text("Objects: %d", app.currentObjectCount); + ImGui::SliderInt("Object Count", &app.ui.objectCountSlider, 1, 1000); + + if (ImGui::Button("Apply")) { + app.desiredObjectCount = app.ui.objectCountSlider; + } + + ImGui::Separator(); + ImGui::InputInt("Delta Count", &app.ui.deltaCount); + if (ImGui::Button("Add Objects")) { + app.desiredObjectCount += app.ui.deltaCount; + } + ImGui::SameLine(); + if (ImGui::Button("Remove Objects")) { + app.desiredObjectCount = std::max(0, app.desiredObjectCount - app.ui.deltaCount); + } + ImGui::Separator(); + + ImGui::SliderFloat("Update Ratio / Frame", &app.ui.updateRatio, 0.0f, 1.0f); + ImGui::Checkbox("Animate", &app.ui.animate); + ImGui::End(); + }; + + auto animate = [&app](Engine* engine, View* view, double now) { + if (app.currentObjectCount != app.desiredObjectCount) { + if (app.desiredObjectCount == app.ui.objectCountSlider) { + clearScene(engine, view->getScene(), app); + engine->flushAndWait(); + createSceneObjects(engine, view->getScene(), app); + } else { + if (app.desiredObjectCount > app.currentObjectCount) { + const int toAdd = app.desiredObjectCount - app.currentObjectCount; + addObjects(engine, view->getScene(), app, toAdd); + } else { + const int toRemove = app.currentObjectCount - app.desiredObjectCount; + removeObjects(engine, view->getScene(), app, toRemove); + } + } + app.ui.objectCountSlider = app.currentObjectCount; + } + + if (!app.ui.animate || app.currentObjectCount == 0) { + return; + } + + const int updateCount = static_cast(app.currentObjectCount * app.ui.updateRatio); + const uint32_t frame = app.frameCounter++; + for (int i = 0; i < updateCount; ++i) { + const int instanceIndex = (frame * i) % app.currentObjectCount; + MaterialInstance* mi = app.materialInstances[instanceIndex]; + + const float x = static_cast(instanceIndex % app.gridDim); + const float y = static_cast(instanceIndex / app.gridDim); + + const float r = 0.5f + 0.5f * sin(now + (x + y) * 0.2f); + const float g = 0.5f + 0.5f * cos(now + (x - y) * 0.2f); + const float b = 0.5f + 0.5f * sin(now * 0.7f + y * 0.2f); + mi->setParameter("baseColor", RgbType::sRGB, float3{r, g, b}); + } + }; + + FilamentApp::get().animate(animate); + FilamentApp::get().run(config, setup, cleanup, gui); + + return 0; +}