From 2c3d2afbf44ad37113baabe2423889ace9a17ebb Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Thu, 5 Feb 2026 19:52:26 +0100 Subject: [PATCH 01/34] Update MeshLoaders example and test list for OBJ PLY STL --- 12_MeshLoaders/CMakeLists.txt | 17 +- 12_MeshLoaders/main.cpp | 1251 +++++++++++++++-- 12_MeshLoaders/meshloaders_inputs.json | 8 + CMakeLists.txt | 3 +- .../examples/common/MonoWindowApplication.hpp | 1 + media | 2 +- 6 files changed, 1188 insertions(+), 94 deletions(-) create mode 100644 12_MeshLoaders/meshloaders_inputs.json diff --git a/12_MeshLoaders/CMakeLists.txt b/12_MeshLoaders/CMakeLists.txt index 709b7d40b..ce026b4e3 100644 --- a/12_MeshLoaders/CMakeLists.txt +++ b/12_MeshLoaders/CMakeLists.txt @@ -1,7 +1,9 @@ set(NBL_INCLUDE_SERACH_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}/include" ) -set(NBL_LIBRARIES) +set(NBL_LIBRARIES + nlohmann_json::nlohmann_json +) if (NBL_BUILD_MITSUBA_LOADER) list(APPEND NBL_INCLUDE_SERACH_DIRECTORIES @@ -23,4 +25,15 @@ endif() add_dependencies(${EXECUTABLE_NAME} argparse) -target_include_directories(${EXECUTABLE_NAME} PUBLIC $) \ No newline at end of file +target_include_directories(${EXECUTABLE_NAME} PUBLIC $) + +add_dependencies(${EXECUTABLE_NAME} nlohmann_json::nlohmann_json) +target_include_directories(${EXECUTABLE_NAME} PUBLIC $) + +enable_testing() + +add_test(NAME NBL_MESHLOADERS_CI + COMMAND "$" --ci + WORKING_DIRECTORY "$" + COMMAND_EXPAND_LISTS +) diff --git a/12_MeshLoaders/main.cpp b/12_MeshLoaders/main.cpp index e27ed4be0..7cdc6b321 100644 --- a/12_MeshLoaders/main.cpp +++ b/12_MeshLoaders/main.cpp @@ -5,7 +5,12 @@ #include "common.hpp" #include "../3rdparty/portable-file-dialogs/portable-file-dialogs.h" -#include +#include "nlohmann/json.hpp" +#include +#include +#include +#include +#include #ifdef NBL_BUILD_MITSUBA_LOADER #include "nbl/ext/MitsubaLoader/CSerializedLoader.h" @@ -14,6 +19,7 @@ #ifdef NBL_BUILD_DEBUG_DRAW #include "nbl/ext/DebugDraw/CDrawAABB.h" #endif +#include "nbl/ext/ScreenShot/ScreenShot.h" class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourcesApplication { @@ -28,6 +34,33 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc DBBM_COUNT }; + enum class RunMode + { + Interactive, + Batch, + CI + }; + + enum class Phase + { + RenderOriginal, + RenderWritten + }; + + struct TestCase + { + std::string name; + nbl::system::path path; + }; + + struct CameraState + { + core::vectorSIMDf position; + core::vectorSIMDf target; + core::matrix4SIMD projection; + float moveSpeed = 1.0f; + }; + public: inline MeshLoadersApp(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD) : IApplicationFramework(_localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD), @@ -43,9 +76,11 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc if (!device_base_t::onAppInitialized(smart_refctd_ptr(system))) return false; + m_runMode = RunMode::Batch; m_saveGeomPrefixPath = localOutputCWD / "saved"; + m_screenshotPrefixPath = localOutputCWD / "screenshots"; + m_testListPath = localInputCWD / "meshloaders_inputs.json"; - // parse args argparse::ArgumentParser parser("12_meshloaders"); parser.add_argument("--savegeometry") .help("Save the mesh on exit or reload") @@ -54,6 +89,15 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc parser.add_argument("--savepath") .nargs(1) .help("Specify the file to which the mesh will be saved"); + parser.add_argument("--ci") + .help("Run in CI mode: load test list, write .ply, capture screenshots, compare data, and exit.") + .flag(); + parser.add_argument("--interactive") + .help("Use file dialog to select a single model.") + .flag(); + parser.add_argument("--testlist") + .nargs(1) + .help("JSON file with test cases. Relative paths are resolved against local input CWD."); try { @@ -66,6 +110,10 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc if (parser["--savegeometry"] == true) m_saveGeom = true; + if (parser["--interactive"] == true) + m_runMode = RunMode::Interactive; + if (parser["--ci"] == true) + m_runMode = RunMode::CI; if (parser.present("--savepath")) { @@ -80,6 +128,20 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc m_specifiedGeomSavePath.emplace(std::move(tmp.generic_string())); } + if (parser.present("--testlist")) + { + auto tmp = path(parser.get("--testlist")); + if (tmp.empty()) + return logFail("Invalid path has been specified in --testlist argument"); + if (tmp.is_relative()) + tmp = localInputCWD / tmp; + m_testListPath = tmp; + } + + if (m_saveGeom) + std::filesystem::create_directories(m_saveGeomPrefixPath); + std::filesystem::create_directories(m_screenshotPrefixPath); + m_semaphore = m_device->createSemaphore(m_realFrameIx); if (!m_semaphore) return logFail("Failed to Create a Semaphore!"); @@ -93,7 +155,6 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc return logFail("Couldn't create Command Buffer!"); } - auto scRes = static_cast(m_surface->getSwapchainResources()); m_renderer = CSimpleDebugRenderer::create(m_assetMgr.get(), scRes->getRenderpass(), 0, {}); if (!m_renderer) @@ -113,9 +174,22 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc } #endif - // - if (!reloadModel()) + if (!initTestCases()) + return false; + + if (isRowViewActive()) + { + m_nonInteractiveTest = false; + if (!loadRowView()) return false; + } + else + { + if (m_runMode != RunMode::Interactive) + m_nonInteractiveTest = true; + if (!startCase(0u)) + return false; + } camera.mapKeysToArrows(); @@ -167,6 +241,7 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc cb->setScissor(0u,1u,¤tRenderArea); } // late latch input + if (!m_nonInteractiveTest) { bool reload = false; camera.beginInputProcessing(nextPresentationTimestamp); @@ -188,12 +263,19 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc ); camera.endInputProcessing(nextPresentationTimestamp); if (reload) - reloadModel(); + reloadInteractive(); } // draw scene - float32_t3x4 viewMatrix = camera.getViewMatrix(); - float32_t4x4 viewProjMatrix = camera.getConcatenatedMatrix(); - m_renderer->render(cb,CSimpleDebugRenderer::SViewParams(viewMatrix,viewProjMatrix)); + float32_t3x4 viewMatrix; + float32_t4x4 viewProjMatrix; + { + // TODO: get rid of legacy matrices + { + memcpy(&viewMatrix,camera.getViewMatrix().pointer(),sizeof(viewMatrix)); + memcpy(&viewProjMatrix,camera.getConcatenatedMatrix().pointer(),sizeof(viewProjMatrix)); + } + m_renderer->render(cb,CSimpleDebugRenderer::SViewParams(viewMatrix,viewProjMatrix)); + } #ifdef NBL_BUILD_DEBUG_DRAW if (m_drawBBMode != DBBM_NONE) { @@ -247,20 +329,28 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc caption += "]"; m_window->setCaption(caption); } + if (isRowViewActive() && !m_rowViewScreenshotCaptured && m_realFrameIx >= RowViewFramesBeforeCapture) + { + if (!captureScreenshot(m_rowViewScreenshotPath, m_loadedScreenshot)) + failExit("Failed to capture row view screenshot."); + m_rowViewScreenshotCaptured = true; + } + advanceCase(); return retval; } inline bool onAppTerminated() override { - if (m_saveGeomTaskFuture.valid()) - { - m_logger->log("Waiting for geometry writer to finish writing...", ILogger::ELL_INFO); - m_saveGeomTaskFuture.wait(); - } - return device_base_t::onAppTerminated(); } + inline bool keepRunning() override + { + if (m_shouldQuit) + return false; + return device_base_t::keepRunning(); + } + protected: const video::IGPURenderpass::SCreationParams::SSubpassDependency* getDefaultSubpassDependencies() const override { @@ -304,27 +394,238 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc private: // TODO: standardise this across examples, and take from `argv` bool m_nonInteractiveTest = false; + bool m_rowViewEnabled = true; + bool m_rowViewScreenshotCaptured = false; - bool reloadModel() + template + [[noreturn]] void failExit(const char* msg, Args... args) { - if (m_nonInteractiveTest) // TODO: maybe also take from argv and argc - m_modelPath = (sharedInputCWD / "ply/Spanner-ply.ply").string(); - else + if (m_logger) + m_logger->log(msg, ILogger::ELL_ERROR, args...); + std::exit(-1); + } + + bool initTestCases() + { + m_cases.clear(); + if (m_runMode == RunMode::Interactive) { - pfd::open_file file("Choose a supported Model File", sharedInputCWD.string(), - { - "All Supported Formats", "*.ply *.stl *.serialized *.obj", - "TODO (.ply)", "*.ply", - "TODO (.stl)", "*.stl", - "Mitsuba 0.6 Serialized (.serialized)", "*.serialized", - "Wavefront Object (.obj)", "*.obj" - }, - false - ); - if (file.result().empty()) - return false; - m_modelPath = file.result()[0]; + system::path picked; + if (!pickModelPath(picked)) + return logFail("No file selected."); + m_cases.push_back({ picked.stem().string(), picked }); + return true; } + return loadTestList(m_testListPath); + } + + bool pickModelPath(system::path& outPath) + { + pfd::open_file file("Choose a supported Model File", sharedInputCWD.string(), + { + "All Supported Formats", "*.ply *.stl *.serialized *.obj", + "TODO (.ply)", "*.ply", + "TODO (.stl)", "*.stl", + "Mitsuba 0.6 Serialized (.serialized)", "*.serialized", + "Wavefront Object (.obj)", "*.obj" + }, + false + ); + if (file.result().empty()) + return false; + outPath = file.result()[0]; + return true; + } + + bool loadTestList(const system::path& jsonPath) + { + if (!std::filesystem::exists(jsonPath)) + return logFail("Missing test list: %s", jsonPath.string().c_str()); + + std::ifstream stream(jsonPath); + if (!stream.is_open()) + return logFail("Failed to open test list: %s", jsonPath.string().c_str()); + + nlohmann::json doc; + try + { + stream >> doc; + } + catch (const std::exception& e) + { + return logFail("Invalid JSON in test list: %s", e.what()); + } + + if (!doc.contains("cases") || !doc["cases"].is_array()) + return logFail("Test list JSON missing \"cases\" array."); + + if (doc.contains("row_view")) + { + if (!doc["row_view"].is_boolean()) + return logFail("\"row_view\" must be a boolean."); + m_rowViewEnabled = doc["row_view"].get(); + } + + for (const auto& entry : doc["cases"]) + { + std::string pathString; + std::string name; + std::string extOverride; + + if (entry.is_string()) + { + pathString = entry.get(); + } + else if (entry.is_object()) + { + if (!entry.contains("path") || !entry["path"].is_string()) + return logFail("Test list entry missing \"path\"."); + pathString = entry["path"].get(); + if (entry.contains("name") && entry["name"].is_string()) + name = entry["name"].get(); + if (entry.contains("extension") && entry["extension"].is_string()) + extOverride = entry["extension"].get(); + } + else + return logFail("Invalid test list entry."); + + system::path path = pathString; + if (path.is_relative()) + path = sharedInputCWD / path; + if (!std::filesystem::exists(path)) + return logFail("Missing test input: %s", path.string().c_str()); + if (!extOverride.empty()) + { + if (path.extension().string() != extOverride) + return logFail("Extension mismatch for %s", path.string().c_str()); + } + + if (name.empty()) + name = path.stem().string(); + + m_cases.push_back({ name, path }); + } + + if (m_cases.empty()) + return logFail("No test cases in test list."); + + return true; + } + + bool isRowViewActive() const + { + return m_rowViewEnabled && m_runMode != RunMode::CI && m_runMode != RunMode::Interactive; + } + + static inline std::string normalizeExtension(const system::path& path) + { + auto ext = path.extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + return ext; + } + + bool isWriteExtensionSupported(const std::string& ext) const + { + if (ext == ".ply" || ext == ".stl") + return true; +#ifdef _NBL_COMPILE_WITH_OBJ_WRITER_ + if (ext == ".obj") + return true; +#endif + return false; + } + + system::path resolveSavePath(const system::path& modelPath) const + { + if (m_specifiedGeomSavePath) + return path(*m_specifiedGeomSavePath); + const auto stem = modelPath.stem().string(); + auto ext = normalizeExtension(modelPath); + if (ext.empty()) + ext = ".ply"; + if (!isWriteExtensionSupported(ext)) + { + if (m_logger) + m_logger->log("No writer for %s, writing .ply instead.", ILogger::ELL_WARNING, ext.c_str()); + ext = ".ply"; + } + return m_saveGeomPrefixPath / (stem + "_written" + ext); + } + + bool startCase(const size_t index) + { + if (index >= m_cases.size()) + return false; + + m_caseIndex = index; + m_phase = Phase::RenderOriginal; + m_phaseFrameCounter = 0u; + m_loadedScreenshot = nullptr; + m_writtenScreenshot = nullptr; + m_referenceCamera.reset(); + m_referenceCpuGeom = nullptr; + m_hasReferenceGeometry = false; + m_hasReferenceGeometryHash = false; + + const auto& testCase = m_cases[m_caseIndex]; + m_caseName = testCase.name.empty() ? testCase.path.stem().string() : testCase.name; + m_writtenPath = resolveSavePath(testCase.path); + m_loadedScreenshotPath = m_screenshotPrefixPath / ("meshloaders_" + m_caseName + "_loaded.png"); + m_writtenScreenshotPath = m_screenshotPrefixPath / ("meshloaders_" + m_caseName + "_written.png"); + + if (!loadModel(testCase.path, true, true)) + return false; + + if (m_currentCpuGeom) + { + m_referenceCpuGeom = m_currentCpuGeom; + m_hasReferenceGeometry = true; + m_referenceGeometryHash = hashGeometry(m_referenceCpuGeom.get()); + m_hasReferenceGeometryHash = true; + } + + return true; + } + + bool advanceToNextCase() + { + const auto nextIndex = m_caseIndex + 1u; + if (nextIndex >= m_cases.size()) + { + m_shouldQuit = true; + return false; + } + if (!startCase(nextIndex)) + { + m_shouldQuit = true; + return false; + } + return true; + } + + void reloadInteractive() + { + system::path picked; + if (!pickModelPath(picked)) + failExit("No file selected."); + if (!loadModel(picked, true, true)) + failExit("Failed to load asset %s.", picked.string().c_str()); + if (m_currentCpuGeom && m_saveGeom) + { + const auto savePath = resolveSavePath(picked); + if (!writeGeometry(m_currentCpuGeom, savePath.string())) + failExit("Geometry write failed."); + } + } + + bool loadModel(const system::path& modelPath, const bool updateCamera, const bool storeCamera) + { + if (modelPath.empty()) + failExit("Empty model path."); + if (!std::filesystem::exists(modelPath)) + failExit("Missing input: %s", modelPath.string().c_str()); + + m_modelPath = modelPath.string(); // free up m_renderer->m_instances.clear(); @@ -336,41 +637,16 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc params.logger = m_logger.get(); auto asset = m_assetMgr->getAsset(m_modelPath, params); if (asset.getContents().empty()) - return false; + failExit("Failed to load asset %s.", m_modelPath.c_str()); // core::vector> geometries; - switch (asset.getAssetType()) - { - case IAsset::E_TYPE::ET_GEOMETRY: - for (const auto& item : asset.getContents()) - if (auto polyGeo = IAsset::castDown(item); polyGeo) - geometries.push_back(polyGeo); - break; - default: - m_logger->log("Asset loaded but not a supported type (ET_GEOMETRY,ET_GEOMETRY_COLLECTION)", ILogger::ELL_ERROR); - break; - } + if (!appendGeometriesFromBundle(asset, geometries)) + failExit("Asset loaded but not a supported type for %s.", m_modelPath.c_str()); if (geometries.empty()) - return false; + failExit("No geometry found in asset %s.", m_modelPath.c_str()); - if (m_saveGeom) - { - if (m_saveGeomTaskFuture.valid()) - { - m_logger->log("Waiting for previous geometry saving task to complete...", ILogger::ELL_INFO); - m_saveGeomTaskFuture.wait(); - } - - std::string currentGeomSavePath = m_specifiedGeomSavePath.value_or((m_saveGeomPrefixPath / path(m_modelPath).filename()).generic_string()); - m_saveGeomTaskFuture = std::async( - std::launch::async, - [this, geometries, currentGeomSavePath] { writeGeometry( - geometries[0], - currentGeomSavePath - ); } - ); - } + m_currentCpuGeom = geometries[0]; using aabb_t = hlsl::shapes::AABB<3, double>; auto printAABB = [&](const aabb_t& aabb, const char* extraMsg = "")->void @@ -411,8 +687,7 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc auto reservation = converter->reserve(inputs); if (!reservation) { - m_logger->log("Failed to reserve GPU objects for CPU->GPU conversion!", ILogger::ELL_ERROR); - return false; + failExit("Failed to reserve GPU objects for CPU->GPU conversion."); } // convert @@ -448,8 +723,7 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc auto future = reservation.convert(cpar); if (future.copy()!=IQueue::RESULT::SUCCESS) { - m_logger->log("Failed to await submission feature!", ILogger::ELL_ERROR); - return false; + failExit("Failed to await submission feature."); } } @@ -466,11 +740,16 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc for (uint32_t i = 0; i < converted.size(); i++) { const auto& geom = converted[i]; - const auto promoted = geom.value->getAABB(); + const auto& cpuGeom = geometries[i].get(); + CPolygonGeometryManipulator::recomputeAABB(cpuGeom); + const auto promoted = cpuGeom->getAABB(); printAABB(promoted,"Geometry"); - tmp[3].x += promoted.getExtent().x; const auto promotedWorld = hlsl::float64_t3x4(worldTforms.emplace_back(hlsl::transpose(tmp))); - const auto transformed = hlsl::shapes::util::transform(promotedWorld,promoted); + const auto translation = hlsl::float64_t3( + static_cast(tmp[3].x), + static_cast(tmp[3].y), + static_cast(tmp[3].z)); + const auto transformed = translateAABB(promoted, translation); printAABB(transformed,"Transformed"); bound = hlsl::shapes::util::union_(transformed,bound); @@ -492,7 +771,6 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc aabbInst.transform = math::linalg::promoted_mul(world4x4, aabbTransform); auto& obbInst = m_obbInstances[i]; - const auto& cpuGeom = geometries[i].get(); const auto obb = CPolygonGeometryManipulator::calculateOBB( cpuGeom->getPositionView().getElementCount(), [geo = cpuGeom, &world4x4](size_t vertex_i) { @@ -507,7 +785,7 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc printAABB(bound,"Total"); if (!m_renderer->addGeometries({ &converted.front().get(),converted.size() })) - return false; + failExit("Failed to add geometries to renderer."); auto worlTformsIt = worldTforms.begin(); for (const auto& geo : m_renderer->getGeometries()) @@ -517,39 +795,807 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc }); } - // get scene bounds and reset camera + if (updateCamera) + { + setupCameraFromAABB(bound); + if (storeCamera) + storeCameraState(); + } + else if (m_referenceCamera) + applyCameraState(*m_referenceCamera); + else + setupCameraFromAABB(bound); + + return true; + } + + bool loadRowView() + { + if (m_cases.empty()) + failExit("No test cases loaded for row view."); + + m_renderer->m_instances.clear(); + m_renderer->clearGeometries({ .semaphore = m_semaphore.get(),.value = m_realFrameIx }); + m_assetMgr->clearAllAssetCache(); + + core::vector> geometries; + core::vector> aabbs; + geometries.reserve(m_cases.size()); + aabbs.reserve(m_cases.size()); + + IAssetLoader::SAssetLoadParams params = {}; + params.logger = m_logger.get(); + + for (const auto& testCase : m_cases) + { + const auto& path = testCase.path; + if (!std::filesystem::exists(path)) + failExit("Missing input: %s", path.string().c_str()); + + auto asset = m_assetMgr->getAsset(path.string(), params); + if (asset.getContents().empty()) + failExit("Failed to load asset %s.", path.string().c_str()); + + smart_refctd_ptr geom; + core::vector> found; + if (appendGeometriesFromBundle(asset, found)) + { + if (!found.empty()) + geom = found.front(); + } + if (!geom) + failExit("No geometry found in asset %s.", path.string().c_str()); + + CPolygonGeometryManipulator::recomputeAABB(geom.get()); + const auto aabb = geom->getAABB>(); + + geometries.push_back(std::move(geom)); + aabbs.push_back(aabb); + } + + if (geometries.empty()) + failExit("No geometry found for row view."); + + using aabb_t = hlsl::shapes::AABB<3, double>; + auto printAABB = [&](const aabb_t& aabb, const char* extraMsg = "")->void + { + m_logger->log("%s AABB is (%f,%f,%f) -> (%f,%f,%f)", ILogger::ELL_INFO, extraMsg, aabb.minVx.x, aabb.minVx.y, aabb.minVx.z, aabb.maxVx.x, aabb.maxVx.y, aabb.maxVx.z); + }; + auto bound = aabb_t::create(); + + smart_refctd_ptr converter = CAssetConverter::create({ .device = m_device.get() }); + const auto transferFamily = getTransferUpQueue()->getFamilyIndex(); + + struct SInputs : CAssetConverter::SInputs + { + virtual inline std::span getSharedOwnershipQueueFamilies(const size_t, const asset::ICPUBuffer*, const CAssetConverter::patch_t&) const + { + return sharedBufferOwnership; + } + + core::vector sharedBufferOwnership; + } inputs = {}; + core::vector> patches(geometries.size(), CSimpleDebugRenderer::DefaultPolygonGeometryPatch); + { + inputs.logger = m_logger.get(); + std::get>(inputs.assets) = { &geometries.front().get(),geometries.size() }; + std::get>(inputs.patches) = patches; + core::unordered_set families; + families.insert(transferFamily); + families.insert(getGraphicsQueue()->getFamilyIndex()); + if (families.size() > 1) + for (const auto fam : families) + inputs.sharedBufferOwnership.push_back(fam); + } + + auto reservation = converter->reserve(inputs); + if (!reservation) + failExit("Failed to reserve GPU objects for CPU->GPU conversion."); + { - const double distance = 0.05; - const auto diagonal = bound.getExtent(); + auto semaphore = m_device->createSemaphore(0u); + + constexpr auto MultiBuffering = 2; + std::array, MultiBuffering> commandBuffers = {}; { - const auto measure = hlsl::length(diagonal); - const auto aspectRatio = float(m_window->getWidth()) / float(m_window->getHeight()); - camera.setProjectionMatrix(hlsl::math::thin_lens::rhPerspectiveFovMatrix(1.2f, aspectRatio, distance * measure * 0.1, measure * 4.0)); - camera.setMoveSpeed(measure * 0.04); + auto pool = m_device->createCommandPool(transferFamily, IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT | IGPUCommandPool::CREATE_FLAGS::TRANSIENT_BIT); + pool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, commandBuffers, smart_refctd_ptr(m_logger)); } - const auto pos = bound.maxVx + diagonal * distance; - camera.setPosition(vectorSIMDf(pos.x, pos.y, pos.z)); - const auto center = (bound.minVx + bound.maxVx) * 0.5; - camera.setTarget(vectorSIMDf(center.x, center.y, center.z)); + commandBuffers.front()->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); + + std::array commandBufferSubmits; + for (auto i = 0; i < MultiBuffering; i++) + commandBufferSubmits[i].cmdbuf = commandBuffers[i].get(); + + SIntendedSubmitInfo transfer = {}; + transfer.queue = getTransferUpQueue(); + transfer.scratchCommandBuffers = commandBufferSubmits; + transfer.scratchSemaphore = { + .semaphore = semaphore.get(), + .value = 0u, + .stageMask = PIPELINE_STAGE_FLAGS::ALL_TRANSFER_BITS + }; + + CAssetConverter::SConvertParams cpar = {}; + cpar.utilities = m_utils.get(); + cpar.transfer = &transfer; + + auto future = reservation.convert(cpar); + if (future.copy() != IQueue::RESULT::SUCCESS) + failExit("Failed to await submission feature."); + } + + double targetExtent = 0.0; + core::vector maxDims; + maxDims.reserve(aabbs.size()); + for (const auto& aabb : aabbs) + { + const auto extent = aabb.getExtent(); + const double maxDim = std::max({ extent.x, extent.y, extent.z, 0.001 }); + maxDims.push_back(maxDim); + if (maxDim > targetExtent) + targetExtent = maxDim; } - // TODO: write out the geometry + core::vector scales; + scales.reserve(aabbs.size()); + for (const auto maxDim : maxDims) + scales.push_back(targetExtent / maxDim); + double maxWidth = 0.0; + double totalWidth = 0.0; + core::vector widths; + widths.reserve(aabbs.size()); + for (size_t i = 0; i < aabbs.size(); ++i) + { + const auto extent = aabbs[i].getExtent(); + const double width = std::max(0.001, extent.x * scales[i]); + widths.push_back(width); + totalWidth += width; + if (width > maxWidth) + maxWidth = width; + } + const double spacing = std::max(0.05 * maxWidth, 0.01); + const double totalSpan = totalWidth + spacing * double(widths.size() > 0 ? widths.size() - 1 : 0); + double cursor = -0.5 * totalSpan; + + auto tmp = hlsl::float32_t4x3( + hlsl::float32_t3(1, 0, 0), + hlsl::float32_t3(0, 1, 0), + hlsl::float32_t3(0, 0, 1), + hlsl::float32_t3(0, 0, 0) + ); + const auto& converted = reservation.getGPUObjects(); + core::vector worldTforms; + worldTforms.reserve(converted.size()); + m_aabbInstances.resize(converted.size()); + m_obbInstances.resize(converted.size()); + + for (uint32_t i = 0; i < converted.size(); i++) + { + const auto& geom = converted[i]; + const auto& cpuGeom = geometries[i].get(); + const auto aabb = aabbs[i]; + printAABB(aabb, "Geometry"); + + const double scale = scales[i]; + const auto center = (aabb.minVx + aabb.maxVx) * 0.5; + const double width = widths[i]; + const double targetCenterX = cursor + 0.5 * width; + cursor += width + spacing; + + const double tx = targetCenterX - scale * center.x; + const double ty = -scale * center.y; + const double tz = -scale * center.z; + tmp[0] = hlsl::float32_t3(static_cast(scale), 0.f, 0.f); + tmp[1] = hlsl::float32_t3(0.f, static_cast(scale), 0.f); + tmp[2] = hlsl::float32_t3(0.f, 0.f, static_cast(scale)); + tmp[3] = hlsl::float32_t3(static_cast(tx), static_cast(ty), static_cast(tz)); + + const auto promotedWorld = hlsl::float64_t3x4(worldTforms.emplace_back(hlsl::transpose(tmp))); + const auto translation = hlsl::float64_t3(tx, ty, tz); + const auto scaled = scaleAABB(aabb, scale); + const auto transformed = translateAABB(scaled, translation); + printAABB(transformed, "Transformed"); + bound = hlsl::shapes::util::union_(transformed, bound); + +#ifdef NBL_BUILD_DEBUG_DRAW + auto& aabbInst = m_aabbInstances[i]; + const auto tmpAabb = shapes::AABB<3, float>(aabb.minVx, aabb.maxVx); + hlsl::float32_t3x4 aabbTransform = ext::debug_draw::DrawAABB::getTransformFromAABB(tmpAabb); + const auto tmpWorld = hlsl::float32_t3x4(promotedWorld); + const auto world4x4 = float32_t4x4{ + tmpWorld[0], + tmpWorld[1], + tmpWorld[2], + float32_t4(0, 0, 0, 1) + }; + aabbInst.color = { 1,1,1,1 }; + aabbInst.transform = math::linalg::promoted_mul(world4x4, aabbTransform); + + auto& obbInst = m_obbInstances[i]; + const auto obb = CPolygonGeometryManipulator::calculateOBB( + cpuGeom->getPositionView().getElementCount(), + [geo = cpuGeom](size_t vertex_i) { + hlsl::float32_t3 pt; + geo->getPositionView().decodeElement(vertex_i, pt); + return pt; + }); + obbInst.color = { 0, 0, 1, 1 }; + obbInst.transform = math::linalg::promoted_mul(world4x4, obb.transform); +#endif + } + + printAABB(bound, "Total"); + if (!m_renderer->addGeometries({ &converted.front().get(),converted.size() })) + failExit("Failed to add geometries to renderer."); + + for (uint32_t i = 0; i < converted.size(); i++) + { + m_renderer->m_instances.push_back({ + .world = worldTforms[i], + .packedGeo = &m_renderer->getGeometry(i) + }); + } + + setupCameraFromAABB(bound); + m_modelPath = "Row view (all meshes)"; + m_rowViewScreenshotPath = m_screenshotPrefixPath / "meshloaders_row_view.png"; + m_rowViewScreenshotCaptured = false; return true; } - void writeGeometry(smart_refctd_ptr geometry, const std::string& savePath) + bool writeGeometry(smart_refctd_ptr geometry, const std::string& savePath) { IAsset* assetPtr = const_cast(static_cast(geometry.get())); - IAssetWriter::SAssetWriteParams params{ assetPtr }; + const auto ext = normalizeExtension(system::path(savePath)); + auto flags = asset::EWF_MESH_IS_RIGHT_HANDED; + if (ext != ".obj") + flags = static_cast(flags | asset::EWF_BINARY); + IAssetWriter::SAssetWriteParams params{ assetPtr, flags }; m_logger->log("Saving mesh to %s", ILogger::ELL_INFO, savePath.c_str()); if (!m_assetMgr->writeAsset(savePath, params)) + { m_logger->log("Failed to save %s", ILogger::ELL_ERROR, savePath.c_str()); + return false; + } m_logger->log("Mesh successfully saved!", ILogger::ELL_INFO); + return true; + } + + void setupCameraFromAABB(const hlsl::shapes::AABB<3, double>& bound) + { + const auto extent = bound.getExtent(); + const auto aspectRatio = double(m_window->getWidth()) / double(m_window->getHeight()); + const double fovY = 1.2; + const double fovX = 2.0 * std::atan(std::tan(fovY * 0.5) * aspectRatio); + const auto center = (bound.minVx + bound.maxVx) * 0.5; + const auto halfExtent = extent * 0.5; + const double halfX = std::max(halfExtent.x, 0.001); + const double halfY = std::max(halfExtent.y, 0.001); + const double halfZ = std::max(halfExtent.z, 0.001); + const double safeRadius = std::max({ halfX, halfY, halfZ }); + + const double distY = halfY / std::tan(fovY * 0.5); + const double distX = halfX / std::tan(fovX * 0.5); + double dist = std::max(distX, distY) + halfZ; + dist *= 1.1; + + const auto dir = hlsl::float64_t3(0.0, 0.0, 1.0); + const auto pos = center + dir * dist; + + const double margin = halfZ * 0.1 + 0.01; + const double nearPlane = std::max(0.001, dist - halfZ - margin); + const double farPlane = dist + halfZ + margin; + + camera.setProjectionMatrix(core::matrix4SIMD::buildProjectionMatrixPerspectiveFovRH(static_cast(fovY), static_cast(aspectRatio), static_cast(nearPlane), static_cast(farPlane))); + camera.setMoveSpeed(static_cast(safeRadius * 0.1)); + camera.setPosition(vectorSIMDf(pos.x, pos.y, pos.z)); + camera.setTarget(vectorSIMDf(center.x, center.y, center.z)); + } + + static inline hlsl::shapes::AABB<3, double> translateAABB(const hlsl::shapes::AABB<3, double>& aabb, const hlsl::float64_t3& translation) + { + auto out = aabb; + out.minVx += translation; + out.maxVx += translation; + return out; + } + + static inline hlsl::shapes::AABB<3, double> scaleAABB(const hlsl::shapes::AABB<3, double>& aabb, const double scale) + { + auto out = aabb; + out.minVx *= scale; + out.maxVx *= scale; + return out; + } + + void storeCameraState() + { + m_referenceCamera = CameraState{ + camera.getPosition(), + camera.getTarget(), + camera.getProjectionMatrix(), + camera.getMoveSpeed() + }; + } + + void applyCameraState(const CameraState& state) + { + camera.setProjectionMatrix(state.projection); + camera.setPosition(state.position); + camera.setTarget(state.target); + camera.setMoveSpeed(state.moveSpeed); + } + + core::blake3_hash_t hashGeometry(const ICPUPolygonGeometry* geo) + { + core::blake3_hasher hasher; + if (!geo) + return static_cast(hasher); + + const auto* indexing = geo->getIndexingCallback(); + const bool hasIndexing = indexing != nullptr; + hasher.update(&hasIndexing, sizeof(hasIndexing)); + if (hasIndexing) + { + const auto topology = indexing->knownTopology(); + hasher << topology; + } + + auto hashView = [&](const ICPUPolygonGeometry::SDataView& view) + { + const bool present = static_cast(view); + hasher.update(&present, sizeof(present)); + if (!present) + return; + + const auto format = view.composed.format; + const auto stride = view.composed.getStride(); + hasher.update(&format, sizeof(format)); + hasher.update(&stride, sizeof(stride)); + hasher.update(&view.src.offset, sizeof(view.src.offset)); + hasher.update(&view.src.size, sizeof(view.src.size)); + const auto rangeFormat = view.composed.rangeFormat; + hasher.update(&rangeFormat, sizeof(rangeFormat)); + view.composed.visitRange([&](const auto& range) + { + hasher.update(&range.minVx, sizeof(range.minVx)); + hasher.update(&range.maxVx, sizeof(range.maxVx)); + }); + + if (view.src.buffer) + { + const auto bufHash = view.src.buffer->computeContentHash(); + hasher << bufHash; + } + }; + + hashView(geo->getPositionView()); + hashView(geo->getNormalView()); + hashView(geo->getIndexView()); + + const auto& auxViews = geo->getAuxAttributeViews(); + const uint64_t auxCount = static_cast(auxViews.size()); + hasher.update(&auxCount, sizeof(auxCount)); + for (const auto& view : auxViews) + hashView(view); + + return static_cast(hasher); + } + + struct GeometryCompareResult + { + uint64_t vertexCountA = 0u; + uint64_t vertexCountB = 0u; + bool hasNormalA = false; + bool hasNormalB = false; + bool hasUvA = false; + bool hasUvB = false; + uint64_t indexCountA = 0u; + uint64_t indexCountB = 0u; + uint64_t posDiffCount = 0u; + double posMaxAbs = 0.0; + uint64_t normalDiffCount = 0u; + double normalMaxAbs = 0.0; + uint64_t uvDiffCount = 0u; + double uvMaxAbs = 0.0; + uint64_t indexDiffCount = 0u; + }; + + const ICPUPolygonGeometry::SDataView* findUvView(const ICPUPolygonGeometry* geo) const + { + if (!geo) + return nullptr; + for (const auto& view : geo->getAuxAttributeViews()) + { + if (!view) + continue; + const auto channels = getFormatChannelCount(view.composed.format); + if (channels >= 2u) + return &view; + } + return nullptr; + } + + bool compareGeometry(const ICPUPolygonGeometry* a, const ICPUPolygonGeometry* b, const double tol, GeometryCompareResult& out) const + { + if (!a || !b) + return false; + + const auto& posA = a->getPositionView(); + const auto& posB = b->getPositionView(); + if (!posA || !posB) + return false; + + out.vertexCountA = posA.getElementCount(); + out.vertexCountB = posB.getElementCount(); + if (out.vertexCountA != out.vertexCountB) + return false; + + auto compareVec = [&](const ICPUPolygonGeometry::SDataView& viewA, const ICPUPolygonGeometry::SDataView& viewB, const uint32_t components, uint64_t& diffCount, double& maxAbs)->bool + { + hlsl::float32_t4 va = {}; + hlsl::float32_t4 vb = {}; + for (uint64_t i = 0; i < out.vertexCountA; ++i) + { + if (!viewA.decodeElement(i, va) || !viewB.decodeElement(i, vb)) + return false; + const float* aVals = &va.x; + const float* bVals = &vb.x; + for (uint32_t c = 0; c < components; ++c) + { + const double diff = std::abs(static_cast(aVals[c]) - static_cast(bVals[c])); + if (diff > maxAbs) + maxAbs = diff; + if (diff > tol) + ++diffCount; + } + } + return true; + }; + + if (!compareVec(posA, posB, 3u, out.posDiffCount, out.posMaxAbs)) + return false; + + const auto& normalA = a->getNormalView(); + const auto& normalB = b->getNormalView(); + out.hasNormalA = static_cast(normalA); + out.hasNormalB = static_cast(normalB); + if (out.hasNormalA != out.hasNormalB) + return false; + if (out.hasNormalA) + if (!compareVec(normalA, normalB, 3u, out.normalDiffCount, out.normalMaxAbs)) + return false; + + const auto* uvA = findUvView(a); + const auto* uvB = findUvView(b); + out.hasUvA = uvA != nullptr; + out.hasUvB = uvB != nullptr; + if (out.hasUvA != out.hasUvB) + return false; + if (out.hasUvA) + if (!compareVec(*uvA, *uvB, 2u, out.uvDiffCount, out.uvMaxAbs)) + return false; + + const auto& idxA = a->getIndexView(); + const auto& idxB = b->getIndexView(); + out.indexCountA = idxA ? idxA.getElementCount() : out.vertexCountA; + out.indexCountB = idxB ? idxB.getElementCount() : out.vertexCountB; + if (out.indexCountA != out.indexCountB) + return false; + + auto getIndex = [&](const ICPUPolygonGeometry::SDataView& view, const uint64_t ix)->uint32_t + { + const void* src = view.getPointer(); + if (!src) + return 0u; + if (view.composed.format == EF_R32_UINT) + return reinterpret_cast(src)[ix]; + if (view.composed.format == EF_R16_UINT) + return static_cast(reinterpret_cast(src)[ix]); + return 0u; + }; + + for (uint64_t i = 0; i < out.indexCountA; ++i) + { + const uint32_t aIdx = idxA ? getIndex(idxA, i) : static_cast(i); + const uint32_t bIdx = idxB ? getIndex(idxB, i) : static_cast(i); + if (aIdx != bIdx) + ++out.indexDiffCount; + } + + return out.posDiffCount == 0u && out.normalDiffCount == 0u && out.uvDiffCount == 0u && out.indexDiffCount == 0u; + } + + bool validateWrittenAsset(const system::path& path) + { + if (!std::filesystem::exists(path)) + return false; + + m_assetMgr->clearAllAssetCache(); + + IAssetLoader::SAssetLoadParams params = {}; + params.logger = m_logger.get(); + auto asset = m_assetMgr->getAsset(path.string(), params); + if (asset.getContents().empty()) + return false; + + core::vector> geometries; + switch (asset.getAssetType()) + { + case IAsset::E_TYPE::ET_GEOMETRY: + for (const auto& item : asset.getContents()) + if (auto polyGeo = IAsset::castDown(item); polyGeo) + geometries.push_back(polyGeo); + break; + default: + return false; + } + return !geometries.empty(); + } + + bool captureScreenshot(const system::path& path, core::smart_refctd_ptr& outImage) + { + if (!m_device || !m_surface || !m_assetMgr) + return false; + + m_device->waitIdle(); + + auto* scRes = static_cast(m_surface->getSwapchainResources()); + auto* fb = scRes ? scRes->getFramebuffer(device_base_t::getCurrentAcquire().imageIndex) : nullptr; + if (!fb) + return false; + + auto colorView = fb->getCreationParameters().colorAttachments[0u]; + if (!colorView) + return false; + + auto cpuView = ext::ScreenShot::createScreenShot( + m_device.get(), + getGraphicsQueue(), + nullptr, + colorView.get(), + asset::ACCESS_FLAGS::COLOR_ATTACHMENT_WRITE_BIT, + asset::IImage::LAYOUT::PRESENT_SRC); + if (!cpuView) + return false; + + if (!path.empty()) + std::filesystem::create_directories(path.parent_path()); + + IAssetWriter::SAssetWriteParams params(cpuView.get()); + if (!m_assetMgr->writeAsset(path.string(), params)) + return false; + + outImage = cpuView; + return true; + } + + bool appendGeometriesFromBundle(const asset::SAssetBundle& bundle, core::vector>& out) const + { + if (bundle.getContents().empty()) + return false; + + switch (bundle.getAssetType()) + { + case IAsset::E_TYPE::ET_GEOMETRY: + for (const auto& item : bundle.getContents()) + { + if (auto polyGeo = IAsset::castDown(item); polyGeo) + out.push_back(polyGeo); + } + break; + case IAsset::E_TYPE::ET_GEOMETRY_COLLECTION: + for (const auto& item : bundle.getContents()) + { + auto collection = IAsset::castDown(item); + if (!collection) + continue; + auto* refs = collection->getGeometries(); + if (!refs) + continue; + for (const auto& ref : *refs) + { + if (!ref.geometry) + continue; + if (ref.geometry->getPrimitiveType() != IGeometryBase::EPrimitiveType::Polygon) + continue; + auto poly = core::smart_refctd_ptr_static_cast(ref.geometry); + if (poly) + out.push_back(poly); + } + } + break; + default: + return false; + } + + return !out.empty(); + } + + bool compareImages(const asset::ICPUImageView* a, const asset::ICPUImageView* b, uint64_t& diffCount, uint8_t& maxDiff) + { + diffCount = 0u; + maxDiff = 0u; + if (!a || !b) + return false; + + const auto* imgA = a->getCreationParameters().image.get(); + const auto* imgB = b->getCreationParameters().image.get(); + if (!imgA || !imgB) + return false; + + const auto paramsA = imgA->getCreationParameters(); + const auto paramsB = imgB->getCreationParameters(); + if (paramsA.format != paramsB.format) + return false; + if (paramsA.extent != paramsB.extent) + return false; + + const auto* bufA = imgA->getBuffer(); + const auto* bufB = imgB->getBuffer(); + if (!bufA || !bufB) + return false; + + const size_t sizeA = bufA->getSize(); + if (sizeA != bufB->getSize()) + return false; + + const auto* dataA = static_cast(bufA->getPointer()); + const auto* dataB = static_cast(bufB->getPointer()); + if (!dataA || !dataB) + return false; + + for (size_t i = 0; i < sizeA; ++i) + { + const uint8_t va = dataA[i]; + const uint8_t vb = dataB[i]; + const uint8_t diff = va > vb ? static_cast(va - vb) : static_cast(vb - va); + if (diff) + { + ++diffCount; + if (diff > maxDiff) + maxDiff = diff; + } + } + + return true; + } + + void advanceCase() + { + if (m_runMode == RunMode::Interactive || m_cases.empty()) + return; + if (isRowViewActive()) + return; + + const uint32_t frameLimit = m_runMode == RunMode::CI ? CiFramesBeforeCapture : NonCiFramesPerCase; + ++m_phaseFrameCounter; + if (m_phaseFrameCounter < frameLimit) + return; + + if (m_phase == Phase::RenderOriginal) + { + if (!captureScreenshot(m_loadedScreenshotPath, m_loadedScreenshot)) + failExit("Failed to capture loaded screenshot."); + + if (m_saveGeom) + { + if (!m_currentCpuGeom) + failExit("No geometry to write."); + if (!writeGeometry(m_currentCpuGeom, m_writtenPath.string())) + failExit("Geometry write failed."); + } + + if (m_runMode == RunMode::CI) + { + if (!loadModel(m_writtenPath, false, false)) + failExit("Failed to load written asset %s.", m_writtenPath.string().c_str()); + if (!m_currentCpuGeom) + failExit("Written geometry missing."); + m_phase = Phase::RenderWritten; + m_phaseFrameCounter = 0u; + return; + } + + if (m_saveGeom) + { + if (!validateWrittenAsset(m_writtenPath)) + failExit("Failed to load written asset %s.", m_writtenPath.string().c_str()); + } + + advanceToNextCase(); + return; + } + + if (m_phase == Phase::RenderWritten) + { + if (!captureScreenshot(m_writtenScreenshotPath, m_writtenScreenshot)) + failExit("Failed to capture written screenshot."); + + if (m_hasReferenceGeometryHash) + { + const auto writtenHash = hashGeometry(m_currentCpuGeom.get()); + if (writtenHash != m_referenceGeometryHash) + { + if (m_hasReferenceGeometry) + { + GeometryCompareResult diff = {}; + const double tol = 1e-5; + const bool compareOk = compareGeometry(m_referenceCpuGeom.get(), m_currentCpuGeom.get(), tol, diff); + m_logger->log("Geometry hash mismatch for %s. CompareOk(%d) Vtx(%llu vs %llu) Idx(%llu vs %llu) PosDiff(%llu max %.8f) NDiff(%llu max %.8f) UvDiff(%llu max %.8f) IdxDiff(%llu) Normals(%d/%d) UV(%d/%d)", + ILogger::ELL_ERROR, + m_caseName.c_str(), + compareOk ? 1 : 0, + static_cast(diff.vertexCountA), + static_cast(diff.vertexCountB), + static_cast(diff.indexCountA), + static_cast(diff.indexCountB), + static_cast(diff.posDiffCount), + diff.posMaxAbs, + static_cast(diff.normalDiffCount), + diff.normalMaxAbs, + static_cast(diff.uvDiffCount), + diff.uvMaxAbs, + static_cast(diff.indexDiffCount), + diff.hasNormalA ? 1 : 0, + diff.hasNormalB ? 1 : 0, + diff.hasUvA ? 1 : 0, + diff.hasUvB ? 1 : 0); + } + failExit("Geometry hash mismatch for %s.", m_caseName.c_str()); + } + } + + if (m_hasReferenceGeometry) + { + GeometryCompareResult diff = {}; + const double tol = 1e-5; + if (!compareGeometry(m_referenceCpuGeom.get(), m_currentCpuGeom.get(), tol, diff)) + { + m_logger->log("Geometry compare failed for %s. Vtx(%llu vs %llu) Idx(%llu vs %llu) PosDiff(%llu max %.8f) NDiff(%llu max %.8f) UvDiff(%llu max %.8f) IdxDiff(%llu) Normals(%d/%d) UV(%d/%d)", + ILogger::ELL_ERROR, + m_caseName.c_str(), + static_cast(diff.vertexCountA), + static_cast(diff.vertexCountB), + static_cast(diff.indexCountA), + static_cast(diff.indexCountB), + static_cast(diff.posDiffCount), + diff.posMaxAbs, + static_cast(diff.normalDiffCount), + diff.normalMaxAbs, + static_cast(diff.uvDiffCount), + diff.uvMaxAbs, + static_cast(diff.indexDiffCount), + diff.hasNormalA ? 1 : 0, + diff.hasNormalB ? 1 : 0, + diff.hasUvA ? 1 : 0, + diff.hasUvB ? 1 : 0); + failExit("Geometry compare failed for %s.", m_caseName.c_str()); + } + } + + uint64_t diffCount = 0u; + uint8_t maxDiff = 0u; + if (!compareImages(m_loadedScreenshot.get(), m_writtenScreenshot.get(), diffCount, maxDiff)) + failExit("Image compare failed for %s.", m_caseName.c_str()); + if (diffCount > MaxImageDiffBytes || maxDiff > MaxImageDiffValue) + failExit("Image diff detected for %s. Bytes: %llu MaxDiff: %u", m_caseName.c_str(), static_cast(diffCount), maxDiff); + if (diffCount != 0u) + m_logger->log("Image diff within tolerance for %s. Bytes: %llu MaxDiff: %u", ILogger::ELL_WARNING, m_caseName.c_str(), static_cast(diffCount), maxDiff); + + advanceToNextCase(); + } } // Maximum frames which can be simultaneously submitted, used to cycle through our per-frame resources like command buffers constexpr static inline uint32_t MaxFramesInFlight = 3u; + constexpr static inline uint32_t CiFramesBeforeCapture = 10u; + constexpr static inline uint32_t NonCiFramesPerCase = 120u; + constexpr static inline uint32_t RowViewFramesBeforeCapture = 10u; + constexpr static inline uint64_t MaxImageDiffBytes = 16u; + constexpr static inline uint8_t MaxImageDiffValue = 1u; // smart_refctd_ptr m_renderer; // @@ -560,11 +1606,12 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc InputSystem::ChannelReader mouse; InputSystem::ChannelReader keyboard; // - Camera camera = Camera(core::vectorSIMDf(0, 0, 0), core::vectorSIMDf(0, 0, 0), hlsl::float32_t4x4()); + Camera camera = Camera(core::vectorSIMDf(0, 0, 0), core::vectorSIMDf(0, 0, 0), core::matrix4SIMD()); // mutables std::string m_modelPath; + std::string m_caseName; - DrawBoundingBoxMode m_drawBBMode; + DrawBoundingBoxMode m_drawBBMode = DBBM_NONE; #ifdef NBL_BUILD_DEBUG_DRAW smart_refctd_ptr m_drawAABB; std::vector m_aabbInstances; @@ -572,10 +1619,34 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc #endif - bool m_saveGeom = false; - std::future m_saveGeomTaskFuture; + bool m_saveGeom = true; std::optional m_specifiedGeomSavePath; nbl::system::path m_saveGeomPrefixPath; + nbl::system::path m_screenshotPrefixPath; + nbl::system::path m_rowViewScreenshotPath; + nbl::system::path m_testListPath; + + RunMode m_runMode = RunMode::Batch; + Phase m_phase = Phase::RenderOriginal; + uint32_t m_phaseFrameCounter = 0u; + size_t m_caseIndex = 0u; + core::vector m_cases; + bool m_shouldQuit = false; + + nbl::system::path m_writtenPath; + nbl::system::path m_loadedScreenshotPath; + nbl::system::path m_writtenScreenshotPath; + + core::smart_refctd_ptr m_currentCpuGeom; + core::smart_refctd_ptr m_referenceCpuGeom; + bool m_hasReferenceGeometry = false; + core::blake3_hash_t m_referenceGeometryHash = {}; + bool m_hasReferenceGeometryHash = false; + + core::smart_refctd_ptr m_loadedScreenshot; + core::smart_refctd_ptr m_writtenScreenshot; + + std::optional m_referenceCamera; }; -NBL_MAIN_FUNC(MeshLoadersApp) \ No newline at end of file +NBL_MAIN_FUNC(MeshLoadersApp) diff --git a/12_MeshLoaders/meshloaders_inputs.json b/12_MeshLoaders/meshloaders_inputs.json new file mode 100644 index 000000000..85cc97d89 --- /dev/null +++ b/12_MeshLoaders/meshloaders_inputs.json @@ -0,0 +1,8 @@ +{ + "row_view": true, + "cases": [ + { "name": "spanner_ply", "extension": ".ply", "path": "ply/Spanner-ply.ply" }, + { "name": "yellowflower_obj", "extension": ".obj", "path": "yellowflower.obj" }, + { "name": "stanford_bunny_stl", "extension": ".stl", "path": "Stanford_Bunny.stl" } + ] +} diff --git a/CMakeLists.txt b/CMakeLists.txt index d945c547a..dffe9b829 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,7 @@ if(NBL_BUILD_EXAMPLES) project(NablaExamples) + enable_testing() if(NBL_BUILD_ANDROID) nbl_android_create_media_storage_apk() @@ -136,4 +137,4 @@ if(NBL_BUILD_EXAMPLES) endforeach() NBL_ADJUST_FOLDERS(examples) -endif() \ No newline at end of file +endif() diff --git a/common/include/nbl/examples/common/MonoWindowApplication.hpp b/common/include/nbl/examples/common/MonoWindowApplication.hpp index a2048b7b0..59c7ece65 100644 --- a/common/include/nbl/examples/common/MonoWindowApplication.hpp +++ b/common/include/nbl/examples/common/MonoWindowApplication.hpp @@ -70,6 +70,7 @@ class MonoWindowApplication : public virtual SimpleWindowedApplication return false; ISwapchain::SCreationParams swapchainParams = { .surface = smart_refctd_ptr(m_surface->getSurface()) }; + swapchainParams.sharedParams.imageUsage |= IGPUImage::E_USAGE_FLAGS::EUF_TRANSFER_SRC_BIT; if (!swapchainParams.deduceFormat(m_physicalDevice)) return logFail("Could not choose a Surface Format for the Swapchain!"); diff --git a/media b/media index 0f7ad42b3..293f204fd 160000 --- a/media +++ b/media @@ -1 +1 @@ -Subproject commit 0f7ad42b33abe3143a5d69c4d14b26cf3e538c88 +Subproject commit 293f204fd0cc0c443d2c732c6adaf4b7e3f9b0d7 From 7130e19108ead0700d05ef65cfcf9c971303c916 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Thu, 5 Feb 2026 20:38:43 +0100 Subject: [PATCH 02/34] Always draw AABB in MeshLoaders --- 12_MeshLoaders/main.cpp | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/12_MeshLoaders/main.cpp b/12_MeshLoaders/main.cpp index 7cdc6b321..5a14092f7 100644 --- a/12_MeshLoaders/main.cpp +++ b/12_MeshLoaders/main.cpp @@ -252,10 +252,6 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc { if (event.keyCode == E_KEY_CODE::EKC_R && event.action == SKeyboardEvent::ECA_RELEASED) reload = true; - if (event.keyCode == E_KEY_CODE::EKC_B && event.action == SKeyboardEvent::ECA_RELEASED) - { - m_drawBBMode = DrawBoundingBoxMode((m_drawBBMode + 1) % DBBM_COUNT); - } } camera.keyboardProcess(events); }, @@ -277,13 +273,12 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc m_renderer->render(cb,CSimpleDebugRenderer::SViewParams(viewMatrix,viewProjMatrix)); } #ifdef NBL_BUILD_DEBUG_DRAW - if (m_drawBBMode != DBBM_NONE) { const ISemaphore::SWaitInfo drawFinished = { .semaphore = m_semaphore.get(),.value = m_realFrameIx + 1u }; ext::debug_draw::DrawAABB::DrawParameters drawParams; drawParams.commandBuffer = cb; drawParams.cameraMat = viewProjMatrix; - m_drawAABB->render(drawParams, drawFinished, m_drawBBMode == DBBM_OBB ? m_obbInstances : m_aabbInstances); + m_drawAABB->render(drawParams, drawFinished, m_aabbInstances); } #endif cb->endRenderPass(); @@ -1611,7 +1606,7 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc std::string m_modelPath; std::string m_caseName; - DrawBoundingBoxMode m_drawBBMode = DBBM_NONE; + DrawBoundingBoxMode m_drawBBMode = DBBM_AABB; #ifdef NBL_BUILD_DEBUG_DRAW smart_refctd_ptr m_drawAABB; std::vector m_aabbInstances; From 97b15e2ef54ba107899db69238551f1543801175 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Thu, 5 Feb 2026 21:09:54 +0100 Subject: [PATCH 03/34] Fix MeshLoaders camera matrices --- 12_MeshLoaders/main.cpp | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/12_MeshLoaders/main.cpp b/12_MeshLoaders/main.cpp index 5a14092f7..8e7e59871 100644 --- a/12_MeshLoaders/main.cpp +++ b/12_MeshLoaders/main.cpp @@ -20,6 +20,7 @@ #include "nbl/ext/DebugDraw/CDrawAABB.h" #endif #include "nbl/ext/ScreenShot/ScreenShot.h" +#include class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourcesApplication { @@ -57,7 +58,7 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc { core::vectorSIMDf position; core::vectorSIMDf target; - core::matrix4SIMD projection; + nbl::hlsl::float32_t4x4 projection; float moveSpeed = 1.0f; }; @@ -262,14 +263,9 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc reloadInteractive(); } // draw scene - float32_t3x4 viewMatrix; - float32_t4x4 viewProjMatrix; + const auto& viewMatrix = camera.getViewMatrix(); + const auto& viewProjMatrix = camera.getConcatenatedMatrix(); { - // TODO: get rid of legacy matrices - { - memcpy(&viewMatrix,camera.getViewMatrix().pointer(),sizeof(viewMatrix)); - memcpy(&viewProjMatrix,camera.getConcatenatedMatrix().pointer(),sizeof(viewProjMatrix)); - } m_renderer->render(cb,CSimpleDebugRenderer::SViewParams(viewMatrix,viewProjMatrix)); } #ifdef NBL_BUILD_DEBUG_DRAW @@ -1083,7 +1079,12 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc const double nearPlane = std::max(0.001, dist - halfZ - margin); const double farPlane = dist + halfZ + margin; - camera.setProjectionMatrix(core::matrix4SIMD::buildProjectionMatrixPerspectiveFovRH(static_cast(fovY), static_cast(aspectRatio), static_cast(nearPlane), static_cast(farPlane))); + const auto projection = nbl::hlsl::buildProjectionMatrixPerspectiveFovRH( + static_cast(fovY), + static_cast(aspectRatio), + static_cast(nearPlane), + static_cast(farPlane)); + camera.setProjectionMatrix(projection); camera.setMoveSpeed(static_cast(safeRadius * 0.1)); camera.setPosition(vectorSIMDf(pos.x, pos.y, pos.z)); camera.setTarget(vectorSIMDf(center.x, center.y, center.z)); @@ -1601,7 +1602,10 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc InputSystem::ChannelReader mouse; InputSystem::ChannelReader keyboard; // - Camera camera = Camera(core::vectorSIMDf(0, 0, 0), core::vectorSIMDf(0, 0, 0), core::matrix4SIMD()); + Camera camera = Camera( + core::vectorSIMDf(0, 0, 0), + core::vectorSIMDf(0, 0, -1), + nbl::hlsl::math::linalg::diagonal(1.0f)); // mutables std::string m_modelPath; std::string m_caseName; From 99454acc4f7dd20cd45b1cad256a94efacdf5b93 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 7 Feb 2026 17:06:04 +0100 Subject: [PATCH 04/34] Stabilize meshloader CI references and validation --- 12_MeshLoaders/README.md | 61 ++ .../bin/references/Spanner-ply.geomhash | 1 + .../bin/references/Stanford_Bunny.geomhash | 1 + .../bin/references/yellowflower.geomhash | 1 + 12_MeshLoaders/main.cpp | 918 ++++++++++++++---- 12_MeshLoaders/meshloaders_inputs.json | 6 +- 6 files changed, 779 insertions(+), 209 deletions(-) create mode 100644 12_MeshLoaders/bin/references/Spanner-ply.geomhash create mode 100644 12_MeshLoaders/bin/references/Stanford_Bunny.geomhash create mode 100644 12_MeshLoaders/bin/references/yellowflower.geomhash diff --git a/12_MeshLoaders/README.md b/12_MeshLoaders/README.md index 6330f4673..622c56af7 100644 --- a/12_MeshLoaders/README.md +++ b/12_MeshLoaders/README.md @@ -1,2 +1,63 @@ +# 12_MeshLoaders + +Loads and writes OBJ, PLY, and STL meshes. Default run reads `meshloaders_inputs.json` from this folder. Relative paths in that file resolve against the JSON file location. + +Modes +- Default: row view if `row_view` is true in the JSON +- `--interactive`: single file dialog +- `--ci`: sequential load, write, reload, hash and image compare, then exit + +Controls (non CI) +- Arrow keys: move camera +- Left mouse drag: rotate +- Home: reset view +- A: add a model to row view +- R: reload test list for row view + +Test list +- `cases` can be a list of strings. Each string is a file path relative to the JSON file. + +Args +- `--testlist ` +- `--savegeometry` +- `--savepath ` +- `--row-add ` +- `--row-duplicate ` + +Performance (Debug, Win11, Ryzen 5 5600G, RTX 4070, 64 GiB RAM) +- Dataset: + - `yellowflower.obj` (104416 bytes) + - `Spanner-ply.ply` (5700266 bytes) + - `Stanford_Bunny.stl` (5620184 bytes) +- Method: + - 9 sequential runs per format + - compared `master_like_oldalgo` vs `latest_optimized` + - measured `getAsset` and `writeAsset` call times from example logs + +Median summary + +| Asset | Load old ms | Load latest ms | Load speedup x | Write old ms | Write latest ms | Write speedup x | +|---|---:|---:|---:|---:|---:|---:| +| `yellowflower.obj` | 31.657 | 25.988 | 1.22 | 543.659 | 156.585 | 3.47 | +| `Spanner-ply.ply` | 1020.151 | 132.630 | 7.69 | 45.458 | 41.828 | 1.09 | +| `Stanford_Bunny.stl` | 36153.774 | 23.387 | 1545.89 | 17324.853 | 209.200 | 82.81 | + +Why old path was slow +- STL loader used tiny scalar reads in binary path (`4` bytes per float), which amplified IO call overhead. +- STL writer emitted many small writes per triangle (`normal + v0 + v1 + v2 + attr`). +- OBJ/PLY writers performed incremental small writes while building text output. +- IO strategy was hardcoded per loader/writer, without one shared policy for tuning. + +Why current path is better +- One shared `SFileIOPolicy` is available in load/write params for all formats. +- Strategy is explicit (`Auto`, `WholeFile`, `Chunked`) with one resolution path and limits. +- `Auto` can use whole-file for small payloads and chunked IO for larger ones. +- Loader perf logs include requested/effective strategy and timing breakdown. + +Raw benchmark data (full per-run tables) +- `tmp/master_vs_latest_debug.md` +- `tmp/bench_masterlike_vs_latest_debug_2026-02-07_v2/raw_runs.csv` +- `tmp/bench_masterlike_vs_latest_debug_2026-02-07_v2/paired_runs.csv` + https://github.com/user-attachments/assets/6f779700-e6d4-4e11-95fb-7a7fddc47255 diff --git a/12_MeshLoaders/bin/references/Spanner-ply.geomhash b/12_MeshLoaders/bin/references/Spanner-ply.geomhash new file mode 100644 index 000000000..1fc5eb419 --- /dev/null +++ b/12_MeshLoaders/bin/references/Spanner-ply.geomhash @@ -0,0 +1 @@ +264ac1d6b5de49770560164959e9f2dcc25ceb9c6474b9510c3c543ea68f8878 diff --git a/12_MeshLoaders/bin/references/Stanford_Bunny.geomhash b/12_MeshLoaders/bin/references/Stanford_Bunny.geomhash new file mode 100644 index 000000000..0cb59bc94 --- /dev/null +++ b/12_MeshLoaders/bin/references/Stanford_Bunny.geomhash @@ -0,0 +1 @@ +77f984c2ffe3211196f4d0f87259607c7fca371ff3c22c6fd6f4d2f0800367b1 diff --git a/12_MeshLoaders/bin/references/yellowflower.geomhash b/12_MeshLoaders/bin/references/yellowflower.geomhash new file mode 100644 index 000000000..90368fb9f --- /dev/null +++ b/12_MeshLoaders/bin/references/yellowflower.geomhash @@ -0,0 +1 @@ +d30d7e4a7f79156fcb18682d871256ac52bc12497bc7f959a150a6191f1074fb diff --git a/12_MeshLoaders/main.cpp b/12_MeshLoaders/main.cpp index 8e7e59871..9da041e4d 100644 --- a/12_MeshLoaders/main.cpp +++ b/12_MeshLoaders/main.cpp @@ -10,6 +10,8 @@ #include #include #include +#include +#include #include #ifdef NBL_BUILD_MITSUBA_LOADER @@ -20,6 +22,7 @@ #include "nbl/ext/DebugDraw/CDrawAABB.h" #endif #include "nbl/ext/ScreenShot/ScreenShot.h" +#include "nbl/system/CFileLogger.h" #include class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourcesApplication @@ -48,12 +51,48 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc RenderWritten }; + enum class RowViewReloadMode + { + Full, + Incremental + }; + struct TestCase { std::string name; nbl::system::path path; }; + struct CachedGeometryEntry + { + smart_refctd_ptr cpu; + video::asset_cached_t gpu; + hlsl::shapes::AABB<3, double> aabb = hlsl::shapes::AABB<3, double>::create(); + bool hasAabb = false; + }; + + struct RowViewPerfStats + { + double totalMs = 0.0; + double clearMs = 0.0; + double loadMs = 0.0; + double extractMs = 0.0; + double aabbMs = 0.0; + double convertMs = 0.0; + double addGeoMs = 0.0; + double layoutMs = 0.0; + double instanceMs = 0.0; + double cameraMs = 0.0; + size_t cases = 0u; + size_t cpuHits = 0u; + size_t cpuMisses = 0u; + size_t gpuHits = 0u; + size_t gpuMisses = 0u; + size_t convertCount = 0u; + size_t addCount = 0u; + bool incremental = false; + }; + struct CameraState { core::vectorSIMDf position; @@ -99,6 +138,18 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc parser.add_argument("--testlist") .nargs(1) .help("JSON file with test cases. Relative paths are resolved against local input CWD."); + parser.add_argument("--row-add") + .nargs(1) + .help("Add a model path to row view on startup without using a dialog."); + parser.add_argument("--row-duplicate") + .nargs(1) + .help("Duplicate the last case N times on startup."); + parser.add_argument("--loader-perf-log") + .nargs(1) + .help("Write loader diagnostics to a file instead of stdout."); + parser.add_argument("--update-references") + .help("Update or create geometry hash references for CI validation.") + .flag(); try { @@ -138,10 +189,58 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc tmp = localInputCWD / tmp; m_testListPath = tmp; } + if (parser.present("--row-add")) + { + auto tmp = path(parser.get("--row-add")); + if (tmp.is_relative()) + tmp = localInputCWD / tmp; + m_rowAddPath = tmp; + } + if (parser.present("--row-duplicate")) + { + auto countStr = parser.get("--row-duplicate"); + try + { + m_rowDuplicateCount = static_cast(std::stoul(countStr)); + } + catch (const std::exception&) + { + return logFail("Invalid --row-duplicate value."); + } + } + if (parser.present("--loader-perf-log")) + { + auto tmp = path(parser.get("--loader-perf-log")); + if (tmp.empty()) + return logFail("Invalid --loader-perf-log value."); + if (tmp.is_relative()) + tmp = localOutputCWD / tmp; + m_loaderPerfLogPath = tmp; + } + if (parser["--update-references"] == true) + m_updateGeometryHashReferences = true; + + m_geometryHashReferenceDir = localInputCWD / "references"; + if (m_geometryHashReferenceDir.empty()) + m_geometryHashReferenceDir = localOutputCWD / "references"; + if (m_runMode == RunMode::CI || m_updateGeometryHashReferences) + { + std::error_code ec; + std::filesystem::create_directories(m_geometryHashReferenceDir, ec); + if (ec) + return logFail("Failed to create geometry hash reference directory: %s", m_geometryHashReferenceDir.string().c_str()); + } if (m_saveGeom) std::filesystem::create_directories(m_saveGeomPrefixPath); std::filesystem::create_directories(m_screenshotPrefixPath); + m_assetLoadLogger = m_logger; + if (m_loaderPerfLogPath) + { + if (!initLoaderPerfLogger(*m_loaderPerfLogPath)) + return false; + m_logger->log("Loader diagnostics will be written to %s", ILogger::ELL_INFO, m_loaderPerfLogPath->string().c_str()); + } m_semaphore = m_device->createSemaphore(m_realFrameIx); if (!m_semaphore) @@ -181,8 +280,18 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc if (isRowViewActive()) { m_nonInteractiveTest = false; - if (!loadRowView()) + if (!loadRowView(RowViewReloadMode::Full)) return false; + if (m_rowAddPath) + if (!addRowViewCaseFromPath(*m_rowAddPath)) + return false; + if (m_rowDuplicateCount > 0u && !m_cases.empty()) + { + const auto lastPath = m_cases.back().path; + for (uint32_t i = 0u; i < m_rowDuplicateCount; ++i) + if (!addRowViewCaseFromPath(lastPath)) + return false; + } } else { @@ -244,22 +353,43 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc // late latch input if (!m_nonInteractiveTest) { - bool reload = false; + bool reloadInteractiveRequested = false; + bool reloadListRequested = false; + bool addRowViewRequested = false; camera.beginInputProcessing(nextPresentationTimestamp); mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void { camera.mouseProcess(events); }, m_logger.get()); keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t& events) -> void { for (const auto& event : events) { - if (event.keyCode == E_KEY_CODE::EKC_R && event.action == SKeyboardEvent::ECA_RELEASED) - reload = true; + if (event.action != SKeyboardEvent::ECA_RELEASED) + continue; + if (event.keyCode == E_KEY_CODE::EKC_R) + { + if (isRowViewActive()) + reloadListRequested = true; + else + reloadInteractiveRequested = true; + } + else if (event.keyCode == E_KEY_CODE::EKC_A) + { + if (isRowViewActive()) + addRowViewRequested = true; + } } camera.keyboardProcess(events); }, m_logger.get() ); camera.endInputProcessing(nextPresentationTimestamp); - if (reload) + if (addRowViewRequested) + addRowViewCase(); + if (reloadListRequested) + { + if (!reloadFromTestList()) + failExit("Failed to reload test list."); + } + if (reloadInteractiveRequested) reloadInteractive(); } // draw scene @@ -399,12 +529,13 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc bool initTestCases() { m_cases.clear(); + m_caseNameCounts.clear(); if (m_runMode == RunMode::Interactive) { system::path picked; if (!pickModelPath(picked)) return logFail("No file selected."); - m_cases.push_back({ picked.stem().string(), picked }); + m_cases.push_back({ makeUniqueCaseName(picked), picked }); return true; } return loadTestList(m_testListPath); @@ -450,6 +581,8 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc if (!doc.contains("cases") || !doc["cases"].is_array()) return logFail("Test list JSON missing \"cases\" array."); + m_caseNameCounts.clear(); + if (doc.contains("row_view")) { if (!doc["row_view"].is_boolean()) @@ -457,11 +590,10 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc m_rowViewEnabled = doc["row_view"].get(); } + const auto baseDir = jsonPath.parent_path(); for (const auto& entry : doc["cases"]) { std::string pathString; - std::string name; - std::string extOverride; if (entry.is_string()) { @@ -472,29 +604,17 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc if (!entry.contains("path") || !entry["path"].is_string()) return logFail("Test list entry missing \"path\"."); pathString = entry["path"].get(); - if (entry.contains("name") && entry["name"].is_string()) - name = entry["name"].get(); - if (entry.contains("extension") && entry["extension"].is_string()) - extOverride = entry["extension"].get(); } else return logFail("Invalid test list entry."); system::path path = pathString; if (path.is_relative()) - path = sharedInputCWD / path; + path = baseDir / path; if (!std::filesystem::exists(path)) return logFail("Missing test input: %s", path.string().c_str()); - if (!extOverride.empty()) - { - if (path.extension().string() != extOverride) - return logFail("Extension mismatch for %s", path.string().c_str()); - } - - if (name.empty()) - name = path.stem().string(); - m_cases.push_back({ name, path }); + m_cases.push_back({ makeUniqueCaseName(path), path }); } if (m_cases.empty()) @@ -543,6 +663,98 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc return m_saveGeomPrefixPath / (stem + "_written" + ext); } + static inline std::string sanitizeCaseNameForFilename(std::string name) + { + for (auto& ch : name) + { + const unsigned char uch = static_cast(ch); + if (!(std::isalnum(uch) || ch == '_' || ch == '-' || ch == '.')) + ch = '_'; + } + if (name.empty()) + name = "unnamed_case"; + return name; + } + + system::path getGeometryHashReferencePath(const std::string& caseName) const + { + return m_geometryHashReferenceDir / (sanitizeCaseNameForFilename(caseName) + ".geomhash"); + } + + static inline std::string geometryHashToHex(const core::blake3_hash_t& hash) + { + static constexpr char HexDigits[] = "0123456789abcdef"; + std::string out; + out.resize(sizeof(hash.data) * 2ull); + for (size_t i = 0ull; i < sizeof(hash.data); ++i) + { + const uint8_t v = hash.data[i]; + out[2ull * i + 0ull] = HexDigits[(v >> 4) & 0xfu]; + out[2ull * i + 1ull] = HexDigits[v & 0xfu]; + } + return out; + } + + static inline bool tryParseNibble(const char c, uint8_t& out) + { + if (c >= '0' && c <= '9') + { + out = static_cast(c - '0'); + return true; + } + if (c >= 'a' && c <= 'f') + { + out = static_cast(10 + c - 'a'); + return true; + } + if (c >= 'A' && c <= 'F') + { + out = static_cast(10 + c - 'A'); + return true; + } + return false; + } + + static inline bool tryParseGeometryHashHex(std::string hex, core::blake3_hash_t& outHash) + { + hex.erase(std::remove_if(hex.begin(), hex.end(), [](unsigned char c) { return std::isspace(c) != 0; }), hex.end()); + if (hex.size() != sizeof(outHash.data) * 2ull) + return false; + + for (size_t i = 0ull; i < sizeof(outHash.data); ++i) + { + uint8_t hi = 0u; + uint8_t lo = 0u; + if (!tryParseNibble(hex[2ull * i + 0ull], hi) || !tryParseNibble(hex[2ull * i + 1ull], lo)) + return false; + outHash.data[i] = static_cast((hi << 4) | lo); + } + return true; + } + + bool readGeometryHashReference(const system::path& refPath, core::blake3_hash_t& outHash) const + { + std::ifstream in(refPath); + if (!in.is_open()) + return false; + std::string line; + std::getline(in, line); + return tryParseGeometryHashHex(std::move(line), outHash); + } + + bool writeGeometryHashReference(const system::path& refPath, const core::blake3_hash_t& hash) const + { + std::error_code ec; + std::filesystem::create_directories(refPath.parent_path(), ec); + if (ec) + return false; + std::ofstream out(refPath, std::ios::binary | std::ios::trunc); + if (!out.is_open()) + return false; + out << geometryHashToHex(hash) << '\n'; + return out.good(); + } + bool startCase(const size_t index) { if (index >= m_cases.size()) @@ -557,6 +769,7 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc m_referenceCpuGeom = nullptr; m_hasReferenceGeometry = false; m_hasReferenceGeometryHash = false; + m_caseGeometryHashReferencePath.clear(); const auto& testCase = m_cases[m_caseIndex]; m_caseName = testCase.name.empty() ? testCase.path.stem().string() : testCase.name; @@ -571,8 +784,38 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc { m_referenceCpuGeom = m_currentCpuGeom; m_hasReferenceGeometry = true; - m_referenceGeometryHash = hashGeometry(m_referenceCpuGeom.get()); + const auto loadedGeometryHash = hashGeometry(m_referenceCpuGeom.get()); + m_referenceGeometryHash = loadedGeometryHash; m_hasReferenceGeometryHash = true; + m_caseGeometryHashReferencePath = getGeometryHashReferencePath(m_caseName); + + if (m_updateGeometryHashReferences) + { + const bool referenceExisted = std::filesystem::exists(m_caseGeometryHashReferencePath); + if (!writeGeometryHashReference(m_caseGeometryHashReferencePath, loadedGeometryHash)) + return logFail("Failed to write geometry hash reference: %s", m_caseGeometryHashReferencePath.string().c_str()); + if (!referenceExisted) + m_logger->log("Geometry hash reference did not exist for %s. Created new reference at %s", ILogger::ELL_WARNING, m_caseName.c_str(), m_caseGeometryHashReferencePath.string().c_str()); + else + m_logger->log("Geometry hash reference updated for %s at %s", ILogger::ELL_INFO, m_caseName.c_str(), m_caseGeometryHashReferencePath.string().c_str()); + } + else if (m_runMode == RunMode::CI) + { + if (!std::filesystem::exists(m_caseGeometryHashReferencePath)) + return logFail("Missing geometry hash reference for %s at %s. Run once with --update-references.", m_caseName.c_str(), m_caseGeometryHashReferencePath.string().c_str()); + + core::blake3_hash_t onDiskHash = {}; + if (!readGeometryHashReference(m_caseGeometryHashReferencePath, onDiskHash)) + return logFail("Invalid geometry hash reference for %s at %s", m_caseName.c_str(), m_caseGeometryHashReferencePath.string().c_str()); + + m_referenceGeometryHash = onDiskHash; + m_hasReferenceGeometryHash = true; + if (loadedGeometryHash != onDiskHash) + { + m_logger->log("Loaded geometry hash mismatch for %s. Current=%s Reference=%s", ILogger::ELL_ERROR, m_caseName.c_str(), geometryHashToHex(loadedGeometryHash).c_str(), geometryHashToHex(onDiskHash).c_str()); + return logFail("Loaded asset differs from stored geometry hash reference for %s.", m_caseName.c_str()); + } + } } return true; @@ -609,6 +852,39 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc } } + bool addRowViewCase() + { + system::path picked; + if (!pickModelPath(picked)) + return false; + return addRowViewCaseFromPath(picked); + } + + bool addRowViewCaseFromPath(const system::path& picked) + { + if (picked.empty()) + return false; + m_cases.push_back({ makeUniqueCaseName(picked), picked }); + m_shouldQuit = false; + return loadRowView(RowViewReloadMode::Incremental); + } + + bool reloadFromTestList() + { + m_cases.clear(); + if (!loadTestList(m_testListPath)) + return false; + m_shouldQuit = false; + m_rowViewScreenshotCaptured = false; + if (isRowViewActive()) + { + m_nonInteractiveTest = false; + return loadRowView(RowViewReloadMode::Full); + } + m_nonInteractiveTest = (m_runMode != RunMode::Interactive); + return startCase(0u); + } + bool loadModel(const system::path& modelPath, const bool updateCamera, const bool storeCamera) { if (modelPath.empty()) @@ -624,9 +900,20 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc m_assetMgr->clearAllAssetCache(); //! load the geometry - IAssetLoader::SAssetLoadParams params = {}; - params.logger = m_logger.get(); + IAssetLoader::SAssetLoadParams params = makeLoadParams(); + using clock_t = std::chrono::high_resolution_clock; + const auto loadStart = clock_t::now(); auto asset = m_assetMgr->getAsset(m_modelPath, params); + const auto loadMs = toMs(clock_t::now() - loadStart); + uintmax_t inputSize = 0u; + if (std::filesystem::exists(modelPath)) + inputSize = std::filesystem::file_size(modelPath); + m_logger->log( + "Asset load call perf: path=%s time=%.3f ms size=%llu", + ILogger::ELL_INFO, + m_modelPath.c_str(), + loadMs, + static_cast(inputSize)); if (asset.getContents().empty()) failExit("Failed to load asset %s.", m_modelPath.c_str()); @@ -727,13 +1014,13 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc core::vector worldTforms; const auto& converted = reservation.getGPUObjects(); m_aabbInstances.resize(converted.size()); - m_obbInstances.resize(converted.size()); + if (m_drawBBMode == DBBM_OBB) + m_obbInstances.resize(converted.size()); for (uint32_t i = 0; i < converted.size(); i++) { const auto& geom = converted[i]; const auto& cpuGeom = geometries[i].get(); - CPolygonGeometryManipulator::recomputeAABB(cpuGeom); - const auto promoted = cpuGeom->getAABB(); + const auto promoted = getGeometryAABB(cpuGeom); printAABB(promoted,"Geometry"); const auto promotedWorld = hlsl::float64_t3x4(worldTforms.emplace_back(hlsl::transpose(tmp))); const auto translation = hlsl::float64_t3( @@ -761,22 +1048,41 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc aabbInst.color = { 1,1,1,1 }; aabbInst.transform = math::linalg::promoted_mul(world4x4, aabbTransform); - auto& obbInst = m_obbInstances[i]; - const auto obb = CPolygonGeometryManipulator::calculateOBB( - cpuGeom->getPositionView().getElementCount(), - [geo = cpuGeom, &world4x4](size_t vertex_i) { - hlsl::float32_t3 pt; - geo->getPositionView().decodeElement(vertex_i, pt); - return pt; - }); - obbInst.color = { 0, 0, 1, 1 }; - obbInst.transform = math::linalg::promoted_mul(world4x4, obb.transform); + if (m_drawBBMode == DBBM_OBB) + { + auto& obbInst = m_obbInstances[i]; + const auto obb = CPolygonGeometryManipulator::calculateOBB( + cpuGeom->getPositionView().getElementCount(), + [geo = cpuGeom, &world4x4](size_t vertex_i) { + hlsl::float32_t3 pt; + geo->getPositionView().decodeElement(vertex_i, pt); + return pt; + }); + obbInst.color = { 0, 0, 1, 1 }; + obbInst.transform = math::linalg::promoted_mul(world4x4, obb.transform); + } #endif } printAABB(bound,"Total"); if (!m_renderer->addGeometries({ &converted.front().get(),converted.size() })) failExit("Failed to add geometries to renderer."); + if (m_logger) + { + const auto& gpuGeos = m_renderer->getGeometries(); + for (size_t geoIx = 0u; geoIx < gpuGeos.size(); ++geoIx) + { + const auto& gpuGeo = gpuGeos[geoIx]; + m_logger->log( + "Renderer geo state: idx=%llu elem=%u posView=%u normalView=%u indexType=%u", + ILogger::ELL_DEBUG, + static_cast(geoIx), + gpuGeo.elementCount, + static_cast(gpuGeo.positionView), + static_cast(gpuGeo.normalView), + static_cast(gpuGeo.indexType)); + } + } auto worlTformsIt = worldTforms.begin(); for (const auto& geo : m_renderer->getGeometries()) @@ -800,22 +1106,36 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc return true; } - bool loadRowView() + bool loadRowView(const RowViewReloadMode mode) { if (m_cases.empty()) failExit("No test cases loaded for row view."); - m_renderer->m_instances.clear(); - m_renderer->clearGeometries({ .semaphore = m_semaphore.get(),.value = m_realFrameIx }); - m_assetMgr->clearAllAssetCache(); + using clock_t = std::chrono::high_resolution_clock; + RowViewPerfStats stats = {}; + stats.incremental = (mode == RowViewReloadMode::Incremental); + stats.cases = m_cases.size(); + const auto totalStart = clock_t::now(); + + const auto clearStart = clock_t::now(); + if (mode == RowViewReloadMode::Full) + { + m_renderer->m_instances.clear(); + m_renderer->clearGeometries({ .semaphore = m_semaphore.get(),.value = m_realFrameIx }); + } + stats.clearMs = toMs(clock_t::now() - clearStart); core::vector> geometries; core::vector> aabbs; geometries.reserve(m_cases.size()); aabbs.reserve(m_cases.size()); - IAssetLoader::SAssetLoadParams params = {}; - params.logger = m_logger.get(); + core::vector> cpuToConvert; + core::vector convertEntries; + + m_rowViewCache.reserve(m_cases.size()); + + IAssetLoader::SAssetLoadParams params = makeLoadParams(); for (const auto& testCase : m_cases) { @@ -823,99 +1143,196 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc if (!std::filesystem::exists(path)) failExit("Missing input: %s", path.string().c_str()); - auto asset = m_assetMgr->getAsset(path.string(), params); - if (asset.getContents().empty()) - failExit("Failed to load asset %s.", path.string().c_str()); - - smart_refctd_ptr geom; - core::vector> found; - if (appendGeometriesFromBundle(asset, found)) + const auto cacheKey = makeCacheKey(path); + auto& entry = m_rowViewCache[cacheKey]; + double assetLoadMs = 0.0; + bool cached = true; + if (!entry.cpu) + { + stats.cpuMisses++; + cached = false; + const auto loadStart = clock_t::now(); + auto asset = m_assetMgr->getAsset(path.string(), params); + assetLoadMs = toMs(clock_t::now() - loadStart); + stats.loadMs += assetLoadMs; + if (asset.getContents().empty()) + failExit("Failed to load asset %s.", path.string().c_str()); + + const auto extractStart = clock_t::now(); + core::vector> found; + if (appendGeometriesFromBundle(asset, found)) + { + if (!found.empty()) + entry.cpu = found.front(); + } + stats.extractMs += toMs(clock_t::now() - extractStart); + if (!entry.cpu) + failExit("No geometry found in asset %s.", path.string().c_str()); + + const auto aabbStart = clock_t::now(); + entry.aabb = getGeometryAABB(entry.cpu.get()); + entry.hasAabb = isValidAABB(entry.aabb); + stats.aabbMs += toMs(clock_t::now() - aabbStart); + } + else { - if (!found.empty()) - geom = found.front(); + stats.cpuHits++; + if (!entry.hasAabb) + { + const auto aabbStart = clock_t::now(); + entry.aabb = getGeometryAABB(entry.cpu.get()); + entry.hasAabb = isValidAABB(entry.aabb); + stats.aabbMs += toMs(clock_t::now() - aabbStart); + } } - if (!geom) - failExit("No geometry found in asset %s.", path.string().c_str()); + logRowViewAssetLoad(path, assetLoadMs, cached); - CPolygonGeometryManipulator::recomputeAABB(geom.get()); - const auto aabb = geom->getAABB>(); + if (!entry.gpu) + { + stats.gpuMisses++; + cpuToConvert.push_back(entry.cpu); + convertEntries.push_back(&entry); + } + else + { + stats.gpuHits++; + } - geometries.push_back(std::move(geom)); - aabbs.push_back(aabb); + geometries.push_back(entry.cpu); + aabbs.push_back(entry.aabb); } if (geometries.empty()) failExit("No geometry found for row view."); + logRowViewLoadTotal(stats.loadMs, stats.cpuHits, stats.cpuMisses); - using aabb_t = hlsl::shapes::AABB<3, double>; - auto printAABB = [&](const aabb_t& aabb, const char* extraMsg = "")->void + if (!cpuToConvert.empty()) + { + stats.convertCount = cpuToConvert.size(); + const auto convertStart = clock_t::now(); + + smart_refctd_ptr converter = CAssetConverter::create({ .device = m_device.get() }); + const auto transferFamily = getTransferUpQueue()->getFamilyIndex(); + + struct SInputs : CAssetConverter::SInputs { - m_logger->log("%s AABB is (%f,%f,%f) -> (%f,%f,%f)", ILogger::ELL_INFO, extraMsg, aabb.minVx.x, aabb.minVx.y, aabb.minVx.z, aabb.maxVx.x, aabb.maxVx.y, aabb.maxVx.z); - }; - auto bound = aabb_t::create(); + virtual inline std::span getSharedOwnershipQueueFamilies(const size_t, const asset::ICPUBuffer*, const CAssetConverter::patch_t&) const + { + return sharedBufferOwnership; + } - smart_refctd_ptr converter = CAssetConverter::create({ .device = m_device.get() }); - const auto transferFamily = getTransferUpQueue()->getFamilyIndex(); + core::vector sharedBufferOwnership; + } inputs = {}; + core::vector> patches(cpuToConvert.size(), CSimpleDebugRenderer::DefaultPolygonGeometryPatch); + { + inputs.logger = m_logger.get(); + std::get>(inputs.assets) = { &cpuToConvert.front().get(),cpuToConvert.size() }; + std::get>(inputs.patches) = patches; + core::unordered_set families; + families.insert(transferFamily); + families.insert(getGraphicsQueue()->getFamilyIndex()); + if (families.size() > 1) + for (const auto fam : families) + inputs.sharedBufferOwnership.push_back(fam); + } + + auto reservation = converter->reserve(inputs); + if (!reservation) + failExit("Failed to reserve GPU objects for CPU->GPU conversion."); - struct SInputs : CAssetConverter::SInputs - { - virtual inline std::span getSharedOwnershipQueueFamilies(const size_t, const asset::ICPUBuffer*, const CAssetConverter::patch_t&) const { - return sharedBufferOwnership; + auto semaphore = m_device->createSemaphore(0u); + + constexpr auto MultiBuffering = 2; + std::array, MultiBuffering> commandBuffers = {}; + { + auto pool = m_device->createCommandPool(transferFamily, IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT | IGPUCommandPool::CREATE_FLAGS::TRANSIENT_BIT); + pool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, commandBuffers, smart_refctd_ptr(m_logger)); + } + commandBuffers.front()->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); + + std::array commandBufferSubmits; + for (auto i = 0; i < MultiBuffering; i++) + commandBufferSubmits[i].cmdbuf = commandBuffers[i].get(); + + SIntendedSubmitInfo transfer = {}; + transfer.queue = getTransferUpQueue(); + transfer.scratchCommandBuffers = commandBufferSubmits; + transfer.scratchSemaphore = { + .semaphore = semaphore.get(), + .value = 0u, + .stageMask = PIPELINE_STAGE_FLAGS::ALL_TRANSFER_BITS + }; + + CAssetConverter::SConvertParams cpar = {}; + cpar.utilities = m_utils.get(); + cpar.transfer = &transfer; + + auto future = reservation.convert(cpar); + if (future.copy() != IQueue::RESULT::SUCCESS) + failExit("Failed to await submission feature."); } - core::vector sharedBufferOwnership; - } inputs = {}; - core::vector> patches(geometries.size(), CSimpleDebugRenderer::DefaultPolygonGeometryPatch); - { - inputs.logger = m_logger.get(); - std::get>(inputs.assets) = { &geometries.front().get(),geometries.size() }; - std::get>(inputs.patches) = patches; - core::unordered_set families; - families.insert(transferFamily); - families.insert(getGraphicsQueue()->getFamilyIndex()); - if (families.size() > 1) - for (const auto fam : families) - inputs.sharedBufferOwnership.push_back(fam); + const auto& converted = reservation.getGPUObjects(); + for (size_t i = 0u; i < converted.size(); ++i) + convertEntries[i]->gpu = converted[i]; + + stats.convertMs = toMs(clock_t::now() - convertStart); } - auto reservation = converter->reserve(inputs); - if (!reservation) - failExit("Failed to reserve GPU objects for CPU->GPU conversion."); + size_t existingCount = m_renderer->getGeometries().size(); + const bool incremental = (mode == RowViewReloadMode::Incremental) && (existingCount <= m_cases.size()); + if (!incremental && mode == RowViewReloadMode::Incremental) + return loadRowView(RowViewReloadMode::Full); + if (mode == RowViewReloadMode::Full) { - auto semaphore = m_device->createSemaphore(0u); - - constexpr auto MultiBuffering = 2; - std::array, MultiBuffering> commandBuffers = {}; + core::vector allGeometries; + allGeometries.reserve(m_cases.size()); + for (const auto& testCase : m_cases) { - auto pool = m_device->createCommandPool(transferFamily, IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT | IGPUCommandPool::CREATE_FLAGS::TRANSIENT_BIT); - pool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, commandBuffers, smart_refctd_ptr(m_logger)); + const auto& entry = m_rowViewCache[makeCacheKey(testCase.path)]; + if (!entry.gpu) + failExit("Missing GPU geometry for %s.", testCase.path.string().c_str()); + allGeometries.push_back(entry.gpu.get()); + } + stats.addCount = allGeometries.size(); + const auto addStart = clock_t::now(); + if (!allGeometries.empty()) + if (!m_renderer->addGeometries({ allGeometries.data(),allGeometries.size() })) + failExit("Failed to add geometries to renderer."); + stats.addGeoMs = toMs(clock_t::now() - addStart); + } + else + { + const size_t addCount = (existingCount < m_cases.size()) ? (m_cases.size() - existingCount) : 0u; + stats.addCount = addCount; + if (addCount > 0u) + { + core::vector newGeometries; + newGeometries.reserve(addCount); + for (size_t i = existingCount; i < m_cases.size(); ++i) + { + const auto& entry = m_rowViewCache[makeCacheKey(m_cases[i].path)]; + if (!entry.gpu) + failExit("Missing GPU geometry for %s.", m_cases[i].path.string().c_str()); + newGeometries.push_back(entry.gpu.get()); + } + const auto addStart = clock_t::now(); + if (!m_renderer->addGeometries({ newGeometries.data(),newGeometries.size() })) + failExit("Failed to add geometries to renderer."); + stats.addGeoMs = toMs(clock_t::now() - addStart); } - commandBuffers.front()->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - - std::array commandBufferSubmits; - for (auto i = 0; i < MultiBuffering; i++) - commandBufferSubmits[i].cmdbuf = commandBuffers[i].get(); - - SIntendedSubmitInfo transfer = {}; - transfer.queue = getTransferUpQueue(); - transfer.scratchCommandBuffers = commandBufferSubmits; - transfer.scratchSemaphore = { - .semaphore = semaphore.get(), - .value = 0u, - .stageMask = PIPELINE_STAGE_FLAGS::ALL_TRANSFER_BITS - }; - - CAssetConverter::SConvertParams cpar = {}; - cpar.utilities = m_utils.get(); - cpar.transfer = &transfer; - - auto future = reservation.convert(cpar); - if (future.copy() != IQueue::RESULT::SUCCESS) - failExit("Failed to await submission feature."); } + using aabb_t = hlsl::shapes::AABB<3, double>; + auto printAABB = [&](const aabb_t& aabb, const char* extraMsg = "")->void + { + m_logger->log("%s AABB is (%f,%f,%f) -> (%f,%f,%f)", ILogger::ELL_INFO, extraMsg, aabb.minVx.x, aabb.minVx.y, aabb.minVx.z, aabb.maxVx.x, aabb.maxVx.y, aabb.maxVx.z); + }; + auto bound = aabb_t::create(); + + const auto layoutStart = clock_t::now(); double targetExtent = 0.0; core::vector maxDims; maxDims.reserve(aabbs.size()); @@ -949,22 +1366,24 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc const double spacing = std::max(0.05 * maxWidth, 0.01); const double totalSpan = totalWidth + spacing * double(widths.size() > 0 ? widths.size() - 1 : 0); double cursor = -0.5 * totalSpan; + stats.layoutMs = toMs(clock_t::now() - layoutStart); + const auto instanceStart = clock_t::now(); auto tmp = hlsl::float32_t4x3( hlsl::float32_t3(1, 0, 0), hlsl::float32_t3(0, 1, 0), hlsl::float32_t3(0, 0, 1), hlsl::float32_t3(0, 0, 0) ); - const auto& converted = reservation.getGPUObjects(); core::vector worldTforms; - worldTforms.reserve(converted.size()); - m_aabbInstances.resize(converted.size()); - m_obbInstances.resize(converted.size()); + worldTforms.reserve(geometries.size()); + m_aabbInstances.resize(geometries.size()); + if (m_drawBBMode == DBBM_OBB) + m_obbInstances.resize(geometries.size()); + m_renderer->m_instances.clear(); - for (uint32_t i = 0; i < converted.size(); i++) + for (uint32_t i = 0; i < geometries.size(); i++) { - const auto& geom = converted[i]; const auto& cpuGeom = geometries[i].get(); const auto aabb = aabbs[i]; printAABB(aabb, "Geometry"); @@ -1004,40 +1423,48 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc aabbInst.color = { 1,1,1,1 }; aabbInst.transform = math::linalg::promoted_mul(world4x4, aabbTransform); - auto& obbInst = m_obbInstances[i]; - const auto obb = CPolygonGeometryManipulator::calculateOBB( - cpuGeom->getPositionView().getElementCount(), - [geo = cpuGeom](size_t vertex_i) { - hlsl::float32_t3 pt; - geo->getPositionView().decodeElement(vertex_i, pt); - return pt; - }); - obbInst.color = { 0, 0, 1, 1 }; - obbInst.transform = math::linalg::promoted_mul(world4x4, obb.transform); + if (m_drawBBMode == DBBM_OBB) + { + auto& obbInst = m_obbInstances[i]; + const auto obb = CPolygonGeometryManipulator::calculateOBB( + cpuGeom->getPositionView().getElementCount(), + [geo = cpuGeom](size_t vertex_i) { + hlsl::float32_t3 pt; + geo->getPositionView().decodeElement(vertex_i, pt); + return pt; + }); + obbInst.color = { 0, 0, 1, 1 }; + obbInst.transform = math::linalg::promoted_mul(world4x4, obb.transform); + } #endif } printAABB(bound, "Total"); - if (!m_renderer->addGeometries({ &converted.front().get(),converted.size() })) - failExit("Failed to add geometries to renderer."); - - for (uint32_t i = 0; i < converted.size(); i++) + for (uint32_t i = 0; i < worldTforms.size(); i++) { m_renderer->m_instances.push_back({ .world = worldTforms[i], .packedGeo = &m_renderer->getGeometry(i) }); } + stats.instanceMs = toMs(clock_t::now() - instanceStart); + const auto cameraStart = clock_t::now(); setupCameraFromAABB(bound); + stats.cameraMs = toMs(clock_t::now() - cameraStart); + m_modelPath = "Row view (all meshes)"; m_rowViewScreenshotPath = m_screenshotPrefixPath / "meshloaders_row_view.png"; m_rowViewScreenshotCaptured = false; + stats.totalMs = toMs(clock_t::now() - totalStart); + logRowViewPerf(stats); return true; } bool writeGeometry(smart_refctd_ptr geometry, const std::string& savePath) { + using clock_t = std::chrono::high_resolution_clock; + const auto start = clock_t::now(); IAsset* assetPtr = const_cast(static_cast(geometry.get())); const auto ext = normalizeExtension(system::path(savePath)); auto flags = asset::EWF_MESH_IS_RIGHT_HANDED; @@ -1047,9 +1474,16 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc m_logger->log("Saving mesh to %s", ILogger::ELL_INFO, savePath.c_str()); if (!m_assetMgr->writeAsset(savePath, params)) { - m_logger->log("Failed to save %s", ILogger::ELL_ERROR, savePath.c_str()); + const auto ms = toMs(clock_t::now() - start); + m_logger->log("Failed to save %s after %.3f ms", ILogger::ELL_ERROR, savePath.c_str(), ms); return false; } + const auto ms = toMs(clock_t::now() - start); + uintmax_t size = 0u; + if (std::filesystem::exists(savePath)) + size = std::filesystem::file_size(savePath); + m_logger->log("Asset write call perf: path=%s ext=%s time=%.3f ms size=%llu", ILogger::ELL_INFO, savePath.c_str(), ext.c_str(), ms, static_cast(size)); + m_logger->log("Writer perf: path=%s ext=%s time=%.3f ms size=%llu", ILogger::ELL_INFO, savePath.c_str(), ext.c_str(), ms, static_cast(size)); m_logger->log("Mesh successfully saved!", ILogger::ELL_INFO); return true; } @@ -1124,60 +1558,143 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc camera.setMoveSpeed(state.moveSpeed); } - core::blake3_hash_t hashGeometry(const ICPUPolygonGeometry* geo) + static bool isValidAABB(const hlsl::shapes::AABB<3, double>& aabb) { - core::blake3_hasher hasher; - if (!geo) - return static_cast(hasher); + return + (aabb.minVx.x <= aabb.maxVx.x) && + (aabb.minVx.y <= aabb.maxVx.y) && + (aabb.minVx.z <= aabb.maxVx.z); + } - const auto* indexing = geo->getIndexingCallback(); - const bool hasIndexing = indexing != nullptr; - hasher.update(&hasIndexing, sizeof(hasIndexing)); - if (hasIndexing) + hlsl::shapes::AABB<3, double> getGeometryAABB(const ICPUPolygonGeometry* geometry) const + { + if (!geometry) + return hlsl::shapes::AABB<3, double>::create(); + auto aabb = geometry->getAABB>(); + if (!isValidAABB(aabb)) { - const auto topology = indexing->knownTopology(); - hasher << topology; + CPolygonGeometryManipulator::recomputeAABB(geometry); + aabb = geometry->getAABB>(); } + return aabb; + } + + system::ILogger* getAssetLoadLogger() const + { + if (m_assetLoadLogger) + return m_assetLoadLogger.get(); + return m_logger.get(); + } + + IAssetLoader::SAssetLoadParams makeLoadParams() const + { + IAssetLoader::SAssetLoadParams params = {}; + params.logger = getAssetLoadLogger(); + return params; + } - auto hashView = [&](const ICPUPolygonGeometry::SDataView& view) + bool initLoaderPerfLogger(const system::path& logPath) + { + if (!m_system) + return logFail("Could not initialize loader perf logger because system is unavailable."); + if (logPath.empty()) + return false; + const auto parent = logPath.parent_path(); + if (!parent.empty()) { - const bool present = static_cast(view); - hasher.update(&present, sizeof(present)); - if (!present) - return; + std::error_code ec; + std::filesystem::create_directories(parent, ec); + if (ec) + return logFail("Could not create loader perf log directory %s", parent.string().c_str()); + } + system::ISystem::future_t> future; + m_system->createFile(future, logPath, system::IFile::ECF_READ_WRITE); + if (!future.wait() || !future.get()) + return logFail("Could not create loader perf log file %s", logPath.string().c_str()); + const auto logMask = core::bitflag(system::ILogger::ELL_ALL); + m_loaderPerfLogger = core::make_smart_refctd_ptr(future.copy(), false, logMask); + m_assetLoadLogger = m_loaderPerfLogger; + return true; + } - const auto format = view.composed.format; - const auto stride = view.composed.getStride(); - hasher.update(&format, sizeof(format)); - hasher.update(&stride, sizeof(stride)); - hasher.update(&view.src.offset, sizeof(view.src.offset)); - hasher.update(&view.src.size, sizeof(view.src.size)); - const auto rangeFormat = view.composed.rangeFormat; - hasher.update(&rangeFormat, sizeof(rangeFormat)); - view.composed.visitRange([&](const auto& range) - { - hasher.update(&range.minVx, sizeof(range.minVx)); - hasher.update(&range.maxVx, sizeof(range.maxVx)); - }); + std::string makeUniqueCaseName(const system::path& path) + { + auto base = path.stem().string(); + if (base.empty()) + base = "case"; + auto& counter = m_caseNameCounts[base]; + std::string name = (counter == 0u) ? base : (base + "_" + std::to_string(counter)); + ++counter; + return name; + } - if (view.src.buffer) - { - const auto bufHash = view.src.buffer->computeContentHash(); - hasher << bufHash; - } - }; + static double toMs(const std::chrono::high_resolution_clock::duration& d) + { + return std::chrono::duration(d).count(); + } + + std::string makeCacheKey(const system::path& path) const + { + return path.lexically_normal().generic_string(); + } - hashView(geo->getPositionView()); - hashView(geo->getNormalView()); - hashView(geo->getIndexView()); + void logRowViewPerf(const RowViewPerfStats& stats) const + { + if (!m_logger) + return; + m_logger->log( + "RowView perf: mode=%s cases=%llu cpuHit=%llu cpuMiss=%llu gpuHit=%llu gpuMiss=%llu convert=%llu add=%llu total=%.3f ms", + ILogger::ELL_INFO, + stats.incremental ? "inc" : "full", + static_cast(stats.cases), + static_cast(stats.cpuHits), + static_cast(stats.cpuMisses), + static_cast(stats.gpuHits), + static_cast(stats.gpuMisses), + static_cast(stats.convertCount), + static_cast(stats.addCount), + stats.totalMs); + m_logger->log( + "RowView perf: clear=%.3f load=%.3f extract=%.3f aabb=%.3f convert=%.3f add=%.3f layout=%.3f inst=%.3f cam=%.3f", + ILogger::ELL_INFO, + stats.clearMs, + stats.loadMs, + stats.extractMs, + stats.aabbMs, + stats.convertMs, + stats.addGeoMs, + stats.layoutMs, + stats.instanceMs, + stats.cameraMs); + } - const auto& auxViews = geo->getAuxAttributeViews(); - const uint64_t auxCount = static_cast(auxViews.size()); - hasher.update(&auxCount, sizeof(auxCount)); - for (const auto& view : auxViews) - hashView(view); + void logRowViewAssetLoad(const system::path& path, const double ms, const bool cached) const + { + if (!m_logger) + return; + m_logger->log( + "RowView perf: asset %s load=%.3f ms%s", + ILogger::ELL_INFO, + path.string().c_str(), + ms, + cached ? " (cached)" : ""); + } - return static_cast(hasher); + void logRowViewLoadTotal(const double ms, const size_t hits, const size_t misses) const + { + if (!m_logger) + return; + m_logger->log( + "RowView perf: asset load total=%.3f ms hits=%llu misses=%llu", + ILogger::ELL_INFO, + ms, + static_cast(hits), + static_cast(misses)); + } + + core::blake3_hash_t hashGeometry(const ICPUPolygonGeometry* geo) + { + return CPolygonGeometryManipulator::computeDeterministicContentHash(geo); } struct GeometryCompareResult @@ -1311,8 +1828,7 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc m_assetMgr->clearAllAssetCache(); - IAssetLoader::SAssetLoadParams params = {}; - params.logger = m_logger.get(); + IAssetLoader::SAssetLoadParams params = makeLoadParams(); auto asset = m_assetMgr->getAsset(path.string(), params); if (asset.getContents().empty()) return false; @@ -1515,32 +2031,12 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc const auto writtenHash = hashGeometry(m_currentCpuGeom.get()); if (writtenHash != m_referenceGeometryHash) { - if (m_hasReferenceGeometry) - { - GeometryCompareResult diff = {}; - const double tol = 1e-5; - const bool compareOk = compareGeometry(m_referenceCpuGeom.get(), m_currentCpuGeom.get(), tol, diff); - m_logger->log("Geometry hash mismatch for %s. CompareOk(%d) Vtx(%llu vs %llu) Idx(%llu vs %llu) PosDiff(%llu max %.8f) NDiff(%llu max %.8f) UvDiff(%llu max %.8f) IdxDiff(%llu) Normals(%d/%d) UV(%d/%d)", - ILogger::ELL_ERROR, - m_caseName.c_str(), - compareOk ? 1 : 0, - static_cast(diff.vertexCountA), - static_cast(diff.vertexCountB), - static_cast(diff.indexCountA), - static_cast(diff.indexCountB), - static_cast(diff.posDiffCount), - diff.posMaxAbs, - static_cast(diff.normalDiffCount), - diff.normalMaxAbs, - static_cast(diff.uvDiffCount), - diff.uvMaxAbs, - static_cast(diff.indexDiffCount), - diff.hasNormalA ? 1 : 0, - diff.hasNormalB ? 1 : 0, - diff.hasUvA ? 1 : 0, - diff.hasUvB ? 1 : 0); - } - failExit("Geometry hash mismatch for %s.", m_caseName.c_str()); + m_logger->log("Geometry hash reference mismatch for %s. Current=%s Reference=%s ReferenceFile=%s", + ILogger::ELL_WARNING, + m_caseName.c_str(), + geometryHashToHex(writtenHash).c_str(), + geometryHashToHex(m_referenceGeometryHash).c_str(), + m_caseGeometryHashReferencePath.empty() ? "" : m_caseGeometryHashReferencePath.string().c_str()); } } @@ -1624,12 +2120,22 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc nbl::system::path m_screenshotPrefixPath; nbl::system::path m_rowViewScreenshotPath; nbl::system::path m_testListPath; + nbl::system::path m_geometryHashReferenceDir; + nbl::system::path m_caseGeometryHashReferencePath; + std::optional m_loaderPerfLogPath; + std::optional m_rowAddPath; + uint32_t m_rowDuplicateCount = 0u; + smart_refctd_ptr m_assetLoadLogger; + smart_refctd_ptr m_loaderPerfLogger; + bool m_updateGeometryHashReferences = false; RunMode m_runMode = RunMode::Batch; Phase m_phase = Phase::RenderOriginal; uint32_t m_phaseFrameCounter = 0u; size_t m_caseIndex = 0u; core::vector m_cases; + std::unordered_map m_caseNameCounts; + std::unordered_map m_rowViewCache; bool m_shouldQuit = false; nbl::system::path m_writtenPath; diff --git a/12_MeshLoaders/meshloaders_inputs.json b/12_MeshLoaders/meshloaders_inputs.json index 85cc97d89..aa1c653f2 100644 --- a/12_MeshLoaders/meshloaders_inputs.json +++ b/12_MeshLoaders/meshloaders_inputs.json @@ -1,8 +1,8 @@ { "row_view": true, "cases": [ - { "name": "spanner_ply", "extension": ".ply", "path": "ply/Spanner-ply.ply" }, - { "name": "yellowflower_obj", "extension": ".obj", "path": "yellowflower.obj" }, - { "name": "stanford_bunny_stl", "extension": ".stl", "path": "Stanford_Bunny.stl" } + "../media/ply/Spanner-ply.ply", + "../media/yellowflower.obj", + "../media/Stanford_Bunny.stl" ] } From 3335a72819fdf6928052a97c6109e7afa888bed0 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sun, 8 Feb 2026 20:22:18 +0100 Subject: [PATCH 05/34] Improve mesh loaders benchmark harness timings --- 12_MeshLoaders/main.cpp | 77 +++++++++++++++++++++++++++++++++++------ 1 file changed, 67 insertions(+), 10 deletions(-) diff --git a/12_MeshLoaders/main.cpp b/12_MeshLoaders/main.cpp index 9da041e4d..79d3fdd7d 100644 --- a/12_MeshLoaders/main.cpp +++ b/12_MeshLoaders/main.cpp @@ -220,9 +220,15 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc if (parser["--update-references"] == true) m_updateGeometryHashReferences = true; - m_geometryHashReferenceDir = localInputCWD / "references"; - if (m_geometryHashReferenceDir.empty()) - m_geometryHashReferenceDir = localOutputCWD / "references"; + const path inputReferencesDir = localInputCWD / "references"; + const path outputReferencesDir = localOutputCWD / "references"; + std::error_code referenceDirEc; + const bool hasInputReferencesDir = std::filesystem::is_directory(inputReferencesDir, referenceDirEc) && !referenceDirEc; + referenceDirEc.clear(); + const bool hasOutputReferencesDir = std::filesystem::is_directory(outputReferencesDir, referenceDirEc) && !referenceDirEc; + m_geometryHashReferenceDir = hasOutputReferencesDir || !hasInputReferencesDir ? outputReferencesDir : inputReferencesDir; + if (hasOutputReferencesDir && !hasInputReferencesDir) + m_logger->log("Geometry hash references resolved to output directory: %s", system::ILogger::ELL_INFO, m_geometryHashReferenceDir.string().c_str()); if (m_runMode == RunMode::CI || m_updateGeometryHashReferences) { std::error_code ec; @@ -891,6 +897,8 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc failExit("Empty model path."); if (!std::filesystem::exists(modelPath)) failExit("Missing input: %s", modelPath.string().c_str()); + using clock_t = std::chrono::high_resolution_clock; + const auto loadOuterStart = clock_t::now(); m_modelPath = modelPath.string(); @@ -901,9 +909,16 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc //! load the geometry IAssetLoader::SAssetLoadParams params = makeLoadParams(); - using clock_t = std::chrono::high_resolution_clock; + const auto openStart = clock_t::now(); + system::ISystem::future_t> loadFileFuture; + m_system->createFile(loadFileFuture, modelPath, system::IFile::ECF_READ); + core::smart_refctd_ptr loadFile; + loadFileFuture.acquire().move_into(loadFile); + const auto openMs = toMs(clock_t::now() - openStart); + if (!loadFile) + failExit("Failed to open input file %s.", modelPath.string().c_str()); const auto loadStart = clock_t::now(); - auto asset = m_assetMgr->getAsset(m_modelPath, params); + auto asset = m_assetMgr->getAsset(loadFile.get(), m_modelPath, params); const auto loadMs = toMs(clock_t::now() - loadStart); uintmax_t inputSize = 0u; if (std::filesystem::exists(modelPath)) @@ -919,10 +934,23 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc // core::vector> geometries; + const auto extractStart = clock_t::now(); if (!appendGeometriesFromBundle(asset, geometries)) failExit("Asset loaded but not a supported type for %s.", m_modelPath.c_str()); + const auto extractMs = toMs(clock_t::now() - extractStart); if (geometries.empty()) failExit("No geometry found in asset %s.", m_modelPath.c_str()); + const auto outerMs = toMs(clock_t::now() - loadOuterStart); + const auto nonLoaderMs = std::max(0.0, outerMs - loadMs); + m_logger->log( + "Asset load outer perf: path=%s open=%.3f ms getAsset=%.3f ms extract=%.3f ms total=%.3f ms non_loader=%.3f ms", + ILogger::ELL_INFO, + m_modelPath.c_str(), + openMs, + loadMs, + extractMs, + outerMs, + nonLoaderMs); m_currentCpuGeom = geometries[0]; @@ -1464,26 +1492,54 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc bool writeGeometry(smart_refctd_ptr geometry, const std::string& savePath) { using clock_t = std::chrono::high_resolution_clock; - const auto start = clock_t::now(); + const auto writeOuterStart = clock_t::now(); IAsset* assetPtr = const_cast(static_cast(geometry.get())); const auto ext = normalizeExtension(system::path(savePath)); auto flags = asset::EWF_MESH_IS_RIGHT_HANDED; if (ext != ".obj") flags = static_cast(flags | asset::EWF_BINARY); IAssetWriter::SAssetWriteParams params{ assetPtr, flags }; + params.logger = getAssetLoadLogger(); m_logger->log("Saving mesh to %s", ILogger::ELL_INFO, savePath.c_str()); - if (!m_assetMgr->writeAsset(savePath, params)) + const auto openStart = clock_t::now(); + system::ISystem::future_t> writeFileFuture; + m_system->createFile(writeFileFuture, system::path(savePath), system::IFile::ECF_WRITE); + core::smart_refctd_ptr writeFile; + writeFileFuture.acquire().move_into(writeFile); + const auto openMs = toMs(clock_t::now() - openStart); + if (!writeFile) + { + m_logger->log("Failed to open output file %s", ILogger::ELL_ERROR, savePath.c_str()); + return false; + } + const auto start = clock_t::now(); + if (!m_assetMgr->writeAsset(writeFile.get(), params)) { const auto ms = toMs(clock_t::now() - start); m_logger->log("Failed to save %s after %.3f ms", ILogger::ELL_ERROR, savePath.c_str(), ms); return false; } - const auto ms = toMs(clock_t::now() - start); + const auto writeMs = toMs(clock_t::now() - start); + const auto statStart = clock_t::now(); uintmax_t size = 0u; if (std::filesystem::exists(savePath)) size = std::filesystem::file_size(savePath); - m_logger->log("Asset write call perf: path=%s ext=%s time=%.3f ms size=%llu", ILogger::ELL_INFO, savePath.c_str(), ext.c_str(), ms, static_cast(size)); - m_logger->log("Writer perf: path=%s ext=%s time=%.3f ms size=%llu", ILogger::ELL_INFO, savePath.c_str(), ext.c_str(), ms, static_cast(size)); + const auto statMs = toMs(clock_t::now() - statStart); + const auto outerMs = toMs(clock_t::now() - writeOuterStart); + const auto nonWriterMs = std::max(0.0, outerMs - writeMs); + m_logger->log("Asset write call perf: path=%s ext=%s time=%.3f ms size=%llu", ILogger::ELL_INFO, savePath.c_str(), ext.c_str(), writeMs, static_cast(size)); + m_logger->log( + "Asset write outer perf: path=%s ext=%s open=%.3f ms writeAsset=%.3f ms stat=%.3f ms total=%.3f ms non_writer=%.3f ms size=%llu", + ILogger::ELL_INFO, + savePath.c_str(), + ext.c_str(), + openMs, + writeMs, + statMs, + outerMs, + nonWriterMs, + static_cast(size)); + m_logger->log("Writer perf: path=%s ext=%s time=%.3f ms size=%llu", ILogger::ELL_INFO, savePath.c_str(), ext.c_str(), writeMs, static_cast(size)); m_logger->log("Mesh successfully saved!", ILogger::ELL_INFO); return true; } @@ -1590,6 +1646,7 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc { IAssetLoader::SAssetLoadParams params = {}; params.logger = getAssetLoadLogger(); + params.cacheFlags = IAssetLoader::ECF_DUPLICATE_TOP_LEVEL; return params; } From 0c843be7927b3060730268f9daf5b23c93ed6930 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Wed, 11 Feb 2026 14:28:19 +0100 Subject: [PATCH 06/34] Add runtime tuning flags and unified path based load timing --- 12_MeshLoaders/main.cpp | 92 +++++++++++++++++++++++++++++++++-------- 1 file changed, 75 insertions(+), 17 deletions(-) diff --git a/12_MeshLoaders/main.cpp b/12_MeshLoaders/main.cpp index 79d3fdd7d..5a64ab35a 100644 --- a/12_MeshLoaders/main.cpp +++ b/12_MeshLoaders/main.cpp @@ -147,6 +147,12 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc parser.add_argument("--loader-perf-log") .nargs(1) .help("Write loader diagnostics to a file instead of stdout."); + parser.add_argument("--loader-content-hashes") + .help("Force loaders to compute CPU buffer content hashes before returning. Enabled by default.") + .flag(); + parser.add_argument("--runtime-tuning") + .nargs(1) + .help("Runtime tuning mode for loaders: none|heuristic|hybrid. Default: heuristic."); parser.add_argument("--update-references") .help("Update or create geometry hash references for CI validation.") .flag(); @@ -219,6 +225,21 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc } if (parser["--update-references"] == true) m_updateGeometryHashReferences = true; + if (parser["--loader-content-hashes"] == true) + m_forceLoaderContentHashes = true; + if (parser.present("--runtime-tuning")) + { + auto mode = parser.get("--runtime-tuning"); + std::transform(mode.begin(), mode.end(), mode.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + if (mode == "none") + m_runtimeTuningMode = asset::SFileIOPolicy::SRuntimeTuning::Mode::None; + else if (mode == "heuristic") + m_runtimeTuningMode = asset::SFileIOPolicy::SRuntimeTuning::Mode::Heuristic; + else if (mode == "hybrid") + m_runtimeTuningMode = asset::SFileIOPolicy::SRuntimeTuning::Mode::Hybrid; + else + return logFail("Invalid --runtime-tuning value. Expected: none|heuristic|hybrid."); + } const path inputReferencesDir = localInputCWD / "references"; const path outputReferencesDir = localOutputCWD / "references"; @@ -909,26 +930,27 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc //! load the geometry IAssetLoader::SAssetLoadParams params = makeLoadParams(); - const auto openStart = clock_t::now(); - system::ISystem::future_t> loadFileFuture; - m_system->createFile(loadFileFuture, modelPath, system::IFile::ECF_READ); - core::smart_refctd_ptr loadFile; - loadFileFuture.acquire().move_into(loadFile); - const auto openMs = toMs(clock_t::now() - openStart); - if (!loadFile) + AssetLoadCallResult loadResult = {}; + if (!loadAssetCallFromPath(modelPath, params, loadResult)) failExit("Failed to open input file %s.", modelPath.string().c_str()); - const auto loadStart = clock_t::now(); - auto asset = m_assetMgr->getAsset(loadFile.get(), m_modelPath, params); - const auto loadMs = toMs(clock_t::now() - loadStart); - uintmax_t inputSize = 0u; - if (std::filesystem::exists(modelPath)) - inputSize = std::filesystem::file_size(modelPath); + if (loadResult.fileFlags != 0u) + { + m_logger->log( + "Input file mapping probe: path=%s flags=0x%X mapped=%d", + ILogger::ELL_PERFORMANCE, + m_modelPath.c_str(), + loadResult.fileFlags, + loadResult.mapped ? 1 : 0); + } + const auto openMs = loadResult.openMs; + const auto loadMs = loadResult.getAssetMs; + auto asset = std::move(loadResult.bundle); m_logger->log( "Asset load call perf: path=%s time=%.3f ms size=%llu", ILogger::ELL_INFO, m_modelPath.c_str(), loadMs, - static_cast(inputSize)); + static_cast(loadResult.inputSize)); if (asset.getContents().empty()) failExit("Failed to load asset %s.", m_modelPath.c_str()); @@ -1179,9 +1201,11 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc { stats.cpuMisses++; cached = false; - const auto loadStart = clock_t::now(); - auto asset = m_assetMgr->getAsset(path.string(), params); - assetLoadMs = toMs(clock_t::now() - loadStart); + AssetLoadCallResult loadResult = {}; + if (!loadAssetCallFromPath(path, params, loadResult)) + failExit("Failed to open input file %s.", path.string().c_str()); + auto asset = std::move(loadResult.bundle); + assetLoadMs = loadResult.getAssetMs; stats.loadMs += assetLoadMs; if (asset.getContents().empty()) failExit("Failed to load asset %s.", path.string().c_str()); @@ -1646,10 +1670,42 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc { IAssetLoader::SAssetLoadParams params = {}; params.logger = getAssetLoadLogger(); + if ((m_runMode == RunMode::CI || isRowViewActive()) && !m_loaderPerfLogger) + params.logger = nullptr; params.cacheFlags = IAssetLoader::ECF_DUPLICATE_TOP_LEVEL; + params.ioPolicy.runtimeTuning.mode = m_runtimeTuningMode; + if (m_forceLoaderContentHashes) + params.loaderFlags = static_cast(params.loaderFlags | IAssetLoader::ELPF_COMPUTE_CONTENT_HASHES); return params; } + struct AssetLoadCallResult + { + asset::SAssetBundle bundle = {}; + double openMs = 0.0; + double getAssetMs = 0.0; + uintmax_t inputSize = 0u; + unsigned fileFlags = 0u; + bool mapped = false; + }; + + bool loadAssetCallFromPath(const system::path& modelPath, const IAssetLoader::SAssetLoadParams& params, AssetLoadCallResult& out) + { + using clock_t = std::chrono::high_resolution_clock; + out.openMs = 0.0; + out.fileFlags = 0u; + out.mapped = false; + if (std::filesystem::exists(modelPath)) + out.inputSize = std::filesystem::file_size(modelPath); + else + out.inputSize = 0u; + + const auto loadStart = clock_t::now(); + out.bundle = m_assetMgr->getAsset(modelPath.string(), params); + out.getAssetMs = toMs(clock_t::now() - loadStart); + return true; + } + bool initLoaderPerfLogger(const system::path& logPath) { if (!m_system) @@ -2185,6 +2241,8 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc smart_refctd_ptr m_assetLoadLogger; smart_refctd_ptr m_loaderPerfLogger; bool m_updateGeometryHashReferences = false; + bool m_forceLoaderContentHashes = true; + asset::SFileIOPolicy::SRuntimeTuning::Mode m_runtimeTuningMode = asset::SFileIOPolicy::SRuntimeTuning::Mode::Heuristic; RunMode m_runMode = RunMode::Batch; Phase m_phase = Phase::RenderOriginal; From feb4ecf10a5cfe4a3cf66b3dc37ab62ba719dcda Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Wed, 11 Feb 2026 15:40:02 +0100 Subject: [PATCH 07/34] Refactor MeshLoaders app split and docs polish --- 12_MeshLoaders/CMakeLists.txt | 20 +- 12_MeshLoaders/MeshLoadersApp.hpp | 251 ++ 12_MeshLoaders/MeshLoadersAppLifecycle.cpp | 850 ++++++ 12_MeshLoaders/MeshLoadersAppLoad.cpp | 799 ++++++ 12_MeshLoaders/MeshLoadersAppRuntime.cpp | 314 +++ 12_MeshLoaders/README.md | 129 +- .../{meshloaders_inputs.json => inputs.json} | 0 12_MeshLoaders/main.cpp | 2267 +---------------- 8 files changed, 2308 insertions(+), 2322 deletions(-) create mode 100644 12_MeshLoaders/MeshLoadersApp.hpp create mode 100644 12_MeshLoaders/MeshLoadersAppLifecycle.cpp create mode 100644 12_MeshLoaders/MeshLoadersAppLoad.cpp create mode 100644 12_MeshLoaders/MeshLoadersAppRuntime.cpp rename 12_MeshLoaders/{meshloaders_inputs.json => inputs.json} (100%) diff --git a/12_MeshLoaders/CMakeLists.txt b/12_MeshLoaders/CMakeLists.txt index ce026b4e3..d45fcba50 100644 --- a/12_MeshLoaders/CMakeLists.txt +++ b/12_MeshLoaders/CMakeLists.txt @@ -1,12 +1,23 @@ -set(NBL_INCLUDE_SERACH_DIRECTORIES +set(SRCs + main.cpp + MeshLoadersApp.hpp + MeshLoadersAppLifecycle.cpp + MeshLoadersAppLoad.cpp + MeshLoadersAppRuntime.cpp + inputs.json + README.md +) + +set(NBL_INCLUDE_SEARCH_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}/include" + "${CMAKE_SOURCE_DIR}/3rdparty" ) set(NBL_LIBRARIES nlohmann_json::nlohmann_json ) if (NBL_BUILD_MITSUBA_LOADER) - list(APPEND NBL_INCLUDE_SERACH_DIRECTORIES + list(APPEND NBL_INCLUDE_SEARCH_DIRECTORIES "${NBL_EXT_MITSUBA_LOADER_INCLUDE_DIRS}" ) list(APPEND NBL_LIBRARIES @@ -14,10 +25,7 @@ if (NBL_BUILD_MITSUBA_LOADER) ) endif() - # TODO; Arek I removed `NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET` from the last parameter here, doesn't this macro have 4 arguments anyway !? -nbl_create_executable_project("" "" "${NBL_INCLUDE_SERACH_DIRECTORIES}" "${NBL_LIBRARIES}") -# TODO: Arek temporarily disabled cause I haven't figured out how to make this target yet -# LINK_BUILTIN_RESOURCES_TO_TARGET(${EXECUTABLE_NAME} nblExamplesGeometrySpirvBRD) +nbl_create_executable_project("${SRCs}" "" "${NBL_INCLUDE_SEARCH_DIRECTORIES}" "${NBL_LIBRARIES}") if (NBL_BUILD_DEBUG_DRAW) target_link_libraries(${EXECUTABLE_NAME} PRIVATE Nabla::ext::DebugDraw) diff --git a/12_MeshLoaders/MeshLoadersApp.hpp b/12_MeshLoaders/MeshLoadersApp.hpp new file mode 100644 index 000000000..e70666d00 --- /dev/null +++ b/12_MeshLoaders/MeshLoadersApp.hpp @@ -0,0 +1,251 @@ +#ifndef _NBL_EXAMPLES_12_MESHLOADERS_APP_H_INCLUDED_ +#define _NBL_EXAMPLES_12_MESHLOADERS_APP_H_INCLUDED_ + +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#include "common.hpp" + +#include +#include +#include +#include +#include +#include + +#ifdef NBL_BUILD_DEBUG_DRAW +#include "nbl/ext/DebugDraw/CDrawAABB.h" +#endif + +class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourcesApplication +{ + using device_base_t = MonoWindowApplication; + using asset_base_t = BuiltinResourcesApplication; + + enum DrawBoundingBoxMode + { + DBBM_NONE, + DBBM_AABB, + DBBM_OBB + }; + + enum class RunMode + { + Interactive, + Batch, + CI + }; + + enum class Phase + { + RenderOriginal, + RenderWritten + }; + + enum class RowViewReloadMode + { + Full, + Incremental + }; + + struct TestCase + { + std::string name; + nbl::system::path path; + }; + + struct CachedGeometryEntry + { + smart_refctd_ptr cpu; + video::asset_cached_t gpu; + hlsl::shapes::AABB<3, double> aabb = hlsl::shapes::AABB<3, double>::create(); + bool hasAabb = false; + }; + + struct RowViewPerfStats + { + double totalMs = 0.0; + double clearMs = 0.0; + double loadMs = 0.0; + double extractMs = 0.0; + double aabbMs = 0.0; + double convertMs = 0.0; + double addGeoMs = 0.0; + double layoutMs = 0.0; + double instanceMs = 0.0; + double cameraMs = 0.0; + size_t cases = 0u; + size_t cpuHits = 0u; + size_t cpuMisses = 0u; + size_t gpuHits = 0u; + size_t gpuMisses = 0u; + size_t convertCount = 0u; + size_t addCount = 0u; + bool incremental = false; + }; + + struct CameraState + { + core::vectorSIMDf position; + core::vectorSIMDf target; + nbl::hlsl::float32_t4x4 projection; + float moveSpeed = 1.0f; + }; + + struct AssetLoadCallResult + { + asset::SAssetBundle bundle = {}; + double getAssetMs = 0.0; + uintmax_t inputSize = 0u; + }; + +public: + MeshLoadersApp(const path& localInputCWD, const path& localOutputCWD, const path& sharedInputCWD, const path& sharedOutputCWD); + + bool onAppInitialized(smart_refctd_ptr&& system) override; + IQueue::SSubmitInfo::SSemaphoreInfo renderFrame(const std::chrono::microseconds nextPresentationTimestamp) override; + bool onAppTerminated() override; + bool keepRunning() override; + +protected: + const video::IGPURenderpass::SCreationParams::SSubpassDependency* getDefaultSubpassDependencies() const override; + +private: + [[noreturn]] void failExit(const char* msg, ...); + + bool initTestCases(); + bool pickModelPath(system::path& outPath); + bool loadTestList(const system::path& jsonPath); + bool isRowViewActive() const; + + static std::string normalizeExtension(const system::path& path); + bool isWriteExtensionSupported(const std::string& ext) const; + system::path resolveSavePath(const system::path& modelPath) const; + + static std::string sanitizeCaseNameForFilename(std::string name); + system::path getGeometryHashReferencePath(const std::string& caseName) const; + static std::string geometryHashToHex(const core::blake3_hash_t& hash); + static bool tryParseNibble(char c, uint8_t& out); + static bool tryParseGeometryHashHex(std::string hex, core::blake3_hash_t& outHash); + bool readGeometryHashReference(const system::path& refPath, core::blake3_hash_t& outHash) const; + bool writeGeometryHashReference(const system::path& refPath, const core::blake3_hash_t& hash) const; + + bool startCase(size_t index); + bool advanceToNextCase(); + void reloadInteractive(); + bool addRowViewCase(); + bool addRowViewCaseFromPath(const system::path& picked); + bool reloadFromTestList(); + + bool loadModel(const system::path& modelPath, bool updateCamera, bool storeCamera); + bool loadRowView(RowViewReloadMode mode); + bool writeGeometry(smart_refctd_ptr geometry, const std::string& savePath); + + void setupCameraFromAABB(const hlsl::shapes::AABB<3, double>& bound); + static hlsl::shapes::AABB<3, double> translateAABB(const hlsl::shapes::AABB<3, double>& aabb, const hlsl::float64_t3& translation); + static hlsl::shapes::AABB<3, double> scaleAABB(const hlsl::shapes::AABB<3, double>& aabb, double scale); + + void storeCameraState(); + void applyCameraState(const CameraState& state); + + static bool isValidAABB(const hlsl::shapes::AABB<3, double>& aabb); + hlsl::shapes::AABB<3, double> getGeometryAABB(const ICPUPolygonGeometry* geometry) const; + + system::ILogger* getAssetLoadLogger() const; + IAssetLoader::SAssetLoadParams makeLoadParams() const; + bool loadAssetCallFromPath(const system::path& modelPath, const IAssetLoader::SAssetLoadParams& params, AssetLoadCallResult& out); + bool initLoaderPerfLogger(const system::path& logPath); + + std::string makeUniqueCaseName(const system::path& path); + static double toMs(const std::chrono::high_resolution_clock::duration& d); + std::string makeCacheKey(const system::path& path) const; + + void logRowViewPerf(const RowViewPerfStats& stats) const; + void logRowViewAssetLoad(const system::path& path, double ms, bool cached) const; + void logRowViewLoadTotal(double ms, size_t hits, size_t misses) const; + + core::blake3_hash_t hashGeometry(const ICPUPolygonGeometry* geo); + bool validateWrittenAsset(const system::path& path); + bool captureScreenshot(const system::path& path, core::smart_refctd_ptr& outImage); + bool appendGeometriesFromBundle(const asset::SAssetBundle& bundle, core::vector>& out) const; + bool compareImages(const asset::ICPUImageView* a, const asset::ICPUImageView* b, uint64_t& diffCount, uint8_t& maxDiff); + + void advanceCase(); + + constexpr static inline uint32_t MaxFramesInFlight = 3u; + constexpr static inline uint32_t CiFramesBeforeCapture = 10u; + constexpr static inline uint32_t NonCiFramesPerCase = 120u; + constexpr static inline uint32_t RowViewFramesBeforeCapture = 10u; + constexpr static inline uint64_t MaxImageDiffBytes = 16u; + constexpr static inline uint8_t MaxImageDiffValue = 1u; + + smart_refctd_ptr m_renderer; + smart_refctd_ptr m_semaphore; + uint64_t m_realFrameIx = 0; + std::array, MaxFramesInFlight> m_cmdBufs; + + InputSystem::ChannelReader mouse; + InputSystem::ChannelReader keyboard; + + Camera camera = Camera( + core::vectorSIMDf(0, 0, 0), + core::vectorSIMDf(0, 0, -1), + nbl::hlsl::math::linalg::diagonal(1.0f)); + + std::string m_modelPath; + std::string m_caseName; + + DrawBoundingBoxMode m_drawBBMode = DBBM_AABB; +#ifdef NBL_BUILD_DEBUG_DRAW + smart_refctd_ptr m_drawAABB; + std::vector m_aabbInstances; + std::vector m_obbInstances; +#endif + + bool m_nonInteractiveTest = false; + bool m_rowViewEnabled = true; + bool m_rowViewScreenshotCaptured = false; + bool m_fileDialogOpen = false; + + bool m_saveGeom = true; + std::optional m_specifiedGeomSavePath; + nbl::system::path m_saveGeomPrefixPath; + nbl::system::path m_screenshotPrefixPath; + nbl::system::path m_rowViewScreenshotPath; + nbl::system::path m_testListPath; + nbl::system::path m_geometryHashReferenceDir; + nbl::system::path m_caseGeometryHashReferencePath; + std::optional m_loaderPerfLogPath; + std::optional m_rowAddPath; + uint32_t m_rowDuplicateCount = 0u; + smart_refctd_ptr m_assetLoadLogger; + smart_refctd_ptr m_loaderPerfLogger; + bool m_updateGeometryHashReferences = false; + bool m_forceLoaderContentHashes = true; + asset::SFileIOPolicy::SRuntimeTuning::Mode m_runtimeTuningMode = asset::SFileIOPolicy::SRuntimeTuning::Mode::Heuristic; + + RunMode m_runMode = RunMode::Batch; + Phase m_phase = Phase::RenderOriginal; + uint32_t m_phaseFrameCounter = 0u; + size_t m_caseIndex = 0u; + core::vector m_cases; + std::unordered_map m_caseNameCounts; + std::unordered_map m_rowViewCache; + bool m_shouldQuit = false; + + nbl::system::path m_writtenPath; + nbl::system::path m_loadedScreenshotPath; + nbl::system::path m_writtenScreenshotPath; + + core::smart_refctd_ptr m_currentCpuGeom; + core::blake3_hash_t m_referenceGeometryHash = {}; + bool m_hasReferenceGeometryHash = false; + + core::smart_refctd_ptr m_loadedScreenshot; + core::smart_refctd_ptr m_writtenScreenshot; + + std::optional m_referenceCamera; +}; + +#endif diff --git a/12_MeshLoaders/MeshLoadersAppLifecycle.cpp b/12_MeshLoaders/MeshLoadersAppLifecycle.cpp new file mode 100644 index 000000000..b290d68ab --- /dev/null +++ b/12_MeshLoaders/MeshLoadersAppLifecycle.cpp @@ -0,0 +1,850 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#include "argparse/argparse.hpp" +#include "portable-file-dialogs/portable-file-dialogs.h" +#include "nlohmann/json.hpp" +#include "MeshLoadersApp.hpp" + +#include +#include +#include +#include +#include +#include + +#ifdef NBL_BUILD_MITSUBA_LOADER +#include "nbl/ext/MitsubaLoader/CSerializedLoader.h" +#endif + +#include "nbl/system/CFileLogger.h" + +MeshLoadersApp::MeshLoadersApp( + const path& localInputCWD, + const path& localOutputCWD, + const path& sharedInputCWD, + const path& sharedOutputCWD) + : IApplicationFramework(localInputCWD, localOutputCWD, sharedInputCWD, sharedOutputCWD) + , device_base_t({1280, 720}, EF_D32_SFLOAT, localInputCWD, localOutputCWD, sharedInputCWD, sharedOutputCWD) +{ +} + +bool MeshLoadersApp::onAppInitialized(smart_refctd_ptr&& system) +{ + if (!asset_base_t::onAppInitialized(smart_refctd_ptr(system))) + return false; +#ifdef NBL_BUILD_MITSUBA_LOADER + m_assetMgr->addAssetLoader(make_smart_refctd_ptr()); +#endif + if (!device_base_t::onAppInitialized(smart_refctd_ptr(system))) + return false; + + m_runMode = RunMode::Batch; + m_saveGeomPrefixPath = localOutputCWD / "saved"; + m_screenshotPrefixPath = localOutputCWD / "screenshots"; + m_testListPath = localInputCWD / "inputs.json"; + + argparse::ArgumentParser parser("12_meshloaders"); + parser.add_argument("--savegeometry") + .help("Save the mesh on exit or reload") + .flag(); + + parser.add_argument("--savepath") + .nargs(1) + .help("Specify the file to which the mesh will be saved"); + parser.add_argument("--ci") + .help("Run in CI mode: load test list, write .ply, capture screenshots, compare data, and exit.") + .flag(); + parser.add_argument("--interactive") + .help("Use file dialog to select a single model.") + .flag(); + parser.add_argument("--testlist") + .nargs(1) + .help("JSON file with test cases. Relative paths are resolved against local input CWD."); + parser.add_argument("--row-add") + .nargs(1) + .help("Add a model path to row view on startup without using a dialog."); + parser.add_argument("--row-duplicate") + .nargs(1) + .help("Duplicate the last case N times on startup."); + parser.add_argument("--loader-perf-log") + .nargs(1) + .help("Write loader diagnostics to a file instead of stdout."); + parser.add_argument("--loader-content-hashes") + .help("Force loaders to compute CPU buffer content hashes before returning. Enabled by default.") + .flag(); + parser.add_argument("--runtime-tuning") + .nargs(1) + .help("Runtime tuning mode for loaders: none|heuristic|hybrid. Default: heuristic."); + parser.add_argument("--update-references") + .help("Update or create geometry hash references for CI validation.") + .flag(); + + try + { + parser.parse_args({ argv.data(), argv.data() + argv.size() }); + } + catch (const std::exception& e) + { + return logFail(e.what()); + } + + if (parser["--savegeometry"] == true) + m_saveGeom = true; + if (parser["--interactive"] == true) + m_runMode = RunMode::Interactive; + if (parser["--ci"] == true) + m_runMode = RunMode::CI; + + if (parser.present("--savepath")) + { + auto tmp = path(parser.get("--savepath")); + + if (tmp.empty() || !tmp.has_filename()) + return logFail("Invalid path has been specified in --savepath argument"); + + if (!std::filesystem::exists(tmp.parent_path())) + return logFail("Path specified in --savepath argument doesn't exist"); + + m_specifiedGeomSavePath.emplace(std::move(tmp.generic_string())); + } + + if (parser.present("--testlist")) + { + auto tmp = path(parser.get("--testlist")); + if (tmp.empty()) + return logFail("Invalid path has been specified in --testlist argument"); + if (tmp.is_relative()) + tmp = localInputCWD / tmp; + m_testListPath = tmp; + } + if (parser.present("--row-add")) + { + auto tmp = path(parser.get("--row-add")); + if (tmp.is_relative()) + tmp = localInputCWD / tmp; + m_rowAddPath = tmp; + } + if (parser.present("--row-duplicate")) + { + auto countStr = parser.get("--row-duplicate"); + try + { + m_rowDuplicateCount = static_cast(std::stoul(countStr)); + } + catch (const std::exception&) + { + return logFail("Invalid --row-duplicate value."); + } + } + if (parser.present("--loader-perf-log")) + { + auto tmp = path(parser.get("--loader-perf-log")); + if (tmp.empty()) + return logFail("Invalid --loader-perf-log value."); + if (tmp.is_relative()) + tmp = localOutputCWD / tmp; + m_loaderPerfLogPath = tmp; + } + if (parser["--update-references"] == true) + m_updateGeometryHashReferences = true; + if (parser["--loader-content-hashes"] == true) + m_forceLoaderContentHashes = true; + if (parser.present("--runtime-tuning")) + { + auto mode = parser.get("--runtime-tuning"); + std::transform(mode.begin(), mode.end(), mode.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + if (mode == "none") + m_runtimeTuningMode = asset::SFileIOPolicy::SRuntimeTuning::Mode::None; + else if (mode == "heuristic") + m_runtimeTuningMode = asset::SFileIOPolicy::SRuntimeTuning::Mode::Heuristic; + else if (mode == "hybrid") + m_runtimeTuningMode = asset::SFileIOPolicy::SRuntimeTuning::Mode::Hybrid; + else + return logFail("Invalid --runtime-tuning value. Expected: none|heuristic|hybrid."); + } + + const path inputReferencesDir = localInputCWD / "references"; + const path outputReferencesDir = localOutputCWD / "references"; + std::error_code referenceDirEc; + const bool hasInputReferencesDir = std::filesystem::is_directory(inputReferencesDir, referenceDirEc) && !referenceDirEc; + referenceDirEc.clear(); + const bool hasOutputReferencesDir = std::filesystem::is_directory(outputReferencesDir, referenceDirEc) && !referenceDirEc; + m_geometryHashReferenceDir = hasOutputReferencesDir || !hasInputReferencesDir ? outputReferencesDir : inputReferencesDir; + if (hasOutputReferencesDir && !hasInputReferencesDir) + m_logger->log("Geometry hash references resolved to output directory: %s", system::ILogger::ELL_INFO, m_geometryHashReferenceDir.string().c_str()); + if (m_runMode == RunMode::CI || m_updateGeometryHashReferences) + { + std::error_code ec; + std::filesystem::create_directories(m_geometryHashReferenceDir, ec); + if (ec) + return logFail("Failed to create geometry hash reference directory: %s", m_geometryHashReferenceDir.string().c_str()); + } + + if (m_saveGeom) + std::filesystem::create_directories(m_saveGeomPrefixPath); + std::filesystem::create_directories(m_screenshotPrefixPath); + m_assetLoadLogger = m_logger; + if (m_loaderPerfLogPath) + { + if (!initLoaderPerfLogger(*m_loaderPerfLogPath)) + return false; + m_logger->log("Loader diagnostics will be written to %s", ILogger::ELL_INFO, m_loaderPerfLogPath->string().c_str()); + } + + m_semaphore = m_device->createSemaphore(m_realFrameIx); + if (!m_semaphore) + return logFail("Failed to Create a Semaphore!"); + + auto pool = m_device->createCommandPool(getGraphicsQueue()->getFamilyIndex(), IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT); + for (auto i = 0u; i < MaxFramesInFlight; i++) + { + if (!pool) + return logFail("Couldn't create Command Pool!"); + if (!pool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, { m_cmdBufs.data() + i,1 })) + return logFail("Couldn't create Command Buffer!"); + } + + auto scRes = static_cast(m_surface->getSwapchainResources()); + m_renderer = CSimpleDebugRenderer::create(m_assetMgr.get(), scRes->getRenderpass(), 0, {}); + if (!m_renderer) + return logFail("Failed to create renderer!"); + +#ifdef NBL_BUILD_DEBUG_DRAW + { + auto* renderpass = scRes->getRenderpass(); + ext::debug_draw::DrawAABB::SCreationParameters params = {}; + params.assetManager = m_assetMgr; + params.transfer = getTransferUpQueue(); + params.drawMode = ext::debug_draw::DrawAABB::ADM_DRAW_BATCH; + params.batchPipelineLayout = ext::debug_draw::DrawAABB::createDefaultPipelineLayout(m_device.get()); + params.renderpass = smart_refctd_ptr(renderpass); + params.utilities = m_utils; + m_drawAABB = ext::debug_draw::DrawAABB::create(std::move(params)); + } +#endif + + if (!initTestCases()) + return false; + + if (isRowViewActive()) + { + m_nonInteractiveTest = false; + if (!loadRowView(RowViewReloadMode::Full)) + return false; + if (m_rowAddPath) + if (!addRowViewCaseFromPath(*m_rowAddPath)) + return false; + if (m_rowDuplicateCount > 0u && !m_cases.empty()) + { + const auto lastPath = m_cases.back().path; + for (uint32_t i = 0u; i < m_rowDuplicateCount; ++i) + if (!addRowViewCaseFromPath(lastPath)) + return false; + } + } + else + { + if (m_runMode != RunMode::Interactive) + m_nonInteractiveTest = true; + if (!startCase(0u)) + return false; + } + + camera.mapKeysToArrows(); + + onAppInitializedFinish(); + return true; +} + +IQueue::SSubmitInfo::SSemaphoreInfo MeshLoadersApp::renderFrame(const std::chrono::microseconds nextPresentationTimestamp) +{ + m_inputSystem->getDefaultMouse(&mouse); + m_inputSystem->getDefaultKeyboard(&keyboard); + + const auto resourceIx = m_realFrameIx % MaxFramesInFlight; + + auto* const cb = m_cmdBufs.data()[resourceIx].get(); + cb->reset(IGPUCommandBuffer::RESET_FLAGS::RELEASE_RESOURCES_BIT); + cb->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); + // clear to black for both things + { + // begin renderpass + { + auto scRes = static_cast(m_surface->getSwapchainResources()); + auto* framebuffer = scRes->getFramebuffer(device_base_t::getCurrentAcquire().imageIndex); + const IGPUCommandBuffer::SClearColorValue clearValue = { .float32 = {1.f,0.f,1.f,1.f} }; + const IGPUCommandBuffer::SClearDepthStencilValue depthValue = { .depth = 0.f }; + const VkRect2D currentRenderArea = + { + .offset = {0,0}, + .extent = {framebuffer->getCreationParameters().width,framebuffer->getCreationParameters().height} + }; + const IGPUCommandBuffer::SRenderpassBeginInfo info = + { + .framebuffer = framebuffer, + .colorClearValues = &clearValue, + .depthStencilClearValues = &depthValue, + .renderArea = currentRenderArea + }; + cb->beginRenderPass(info, IGPUCommandBuffer::SUBPASS_CONTENTS::INLINE); + + const SViewport viewport = { + .x = static_cast(currentRenderArea.offset.x), + .y = static_cast(currentRenderArea.offset.y), + .width = static_cast(currentRenderArea.extent.width), + .height = static_cast(currentRenderArea.extent.height) + }; + cb->setViewport(0u,1u,&viewport); + + cb->setScissor(0u,1u,¤tRenderArea); + } + // late latch input + if (!m_nonInteractiveTest) + { + bool reloadInteractiveRequested = false; + bool reloadListRequested = false; + bool addRowViewRequested = false; + camera.beginInputProcessing(nextPresentationTimestamp); + mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void { camera.mouseProcess(events); }, m_logger.get()); + keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t& events) -> void + { + for (const auto& event : events) + { + if (event.action != SKeyboardEvent::ECA_RELEASED) + continue; + if (event.keyCode == E_KEY_CODE::EKC_R) + { + if (isRowViewActive()) + reloadListRequested = true; + else + reloadInteractiveRequested = true; + } + else if (event.keyCode == E_KEY_CODE::EKC_A) + { + if (isRowViewActive()) + addRowViewRequested = true; + } + } + camera.keyboardProcess(events); + }, + m_logger.get() + ); + camera.endInputProcessing(nextPresentationTimestamp); + if (addRowViewRequested) + addRowViewCase(); + if (reloadListRequested) + { + if (!reloadFromTestList()) + failExit("Failed to reload test list."); + } + if (reloadInteractiveRequested) + reloadInteractive(); + } + // draw scene + const auto& viewMatrix = camera.getViewMatrix(); + const auto& viewProjMatrix = camera.getConcatenatedMatrix(); + { + m_renderer->render(cb,CSimpleDebugRenderer::SViewParams(viewMatrix,viewProjMatrix)); + } +#ifdef NBL_BUILD_DEBUG_DRAW + { + const ISemaphore::SWaitInfo drawFinished = { .semaphore = m_semaphore.get(),.value = m_realFrameIx + 1u }; + ext::debug_draw::DrawAABB::DrawParameters drawParams; + drawParams.commandBuffer = cb; + drawParams.cameraMat = viewProjMatrix; + m_drawAABB->render(drawParams, drawFinished, m_aabbInstances); + } +#endif + cb->endRenderPass(); + } + cb->end(); + + IQueue::SSubmitInfo::SSemaphoreInfo retval = + { + .semaphore = m_semaphore.get(), + .value = ++m_realFrameIx, + .stageMask = PIPELINE_STAGE_FLAGS::ALL_GRAPHICS_BITS + }; + const IQueue::SSubmitInfo::SCommandBufferInfo commandBuffers[] = + { + {.cmdbuf = cb } + }; + const IQueue::SSubmitInfo::SSemaphoreInfo acquired[] = { + { + .semaphore = device_base_t::getCurrentAcquire().semaphore, + .value = device_base_t::getCurrentAcquire().acquireCount, + .stageMask = PIPELINE_STAGE_FLAGS::NONE + } + }; + const IQueue::SSubmitInfo infos[] = + { + { + .waitSemaphores = acquired, + .commandBuffers = commandBuffers, + .signalSemaphores = {&retval,1} + } + }; + + if (getGraphicsQueue()->submit(infos) != IQueue::RESULT::SUCCESS) + { + retval.semaphore = nullptr; // so that we don't wait on semaphore that will never signal + m_realFrameIx--; + } + + std::string caption = "[Nabla Engine] Mesh Loaders"; + { + caption += ", displaying ["; + caption += m_modelPath; + caption += "]"; + m_window->setCaption(caption); + } + if (isRowViewActive() && !m_rowViewScreenshotCaptured && m_realFrameIx >= RowViewFramesBeforeCapture) + { + if (!captureScreenshot(m_rowViewScreenshotPath, m_loadedScreenshot)) + failExit("Failed to capture row view screenshot."); + m_rowViewScreenshotCaptured = true; + } + advanceCase(); + return retval; +} + +bool MeshLoadersApp::onAppTerminated() +{ + return device_base_t::onAppTerminated(); +} + +bool MeshLoadersApp::keepRunning() +{ + if (m_shouldQuit) + return false; + return device_base_t::keepRunning(); +} + +const video::IGPURenderpass::SCreationParams::SSubpassDependency* MeshLoadersApp::getDefaultSubpassDependencies() const +{ + // Subsequent submits don't wait for each other, hence its important to have External Dependencies which prevent users of the depth attachment overlapping. + const static IGPURenderpass::SCreationParams::SSubpassDependency dependencies[] = { + // wipe-transition of Color to ATTACHMENT_OPTIMAL and depth + { + .srcSubpass = IGPURenderpass::SCreationParams::SSubpassDependency::External, + .dstSubpass = 0, + .memoryBarrier = { + // last place where the depth can get modified in previous frame, `COLOR_ATTACHMENT_OUTPUT_BIT` is implicitly later + .srcStageMask = PIPELINE_STAGE_FLAGS::LATE_FRAGMENT_TESTS_BIT, + // don't want any writes to be available, we'll clear + .srcAccessMask = ACCESS_FLAGS::NONE, + // destination needs to wait as early as possible + .dstStageMask = PIPELINE_STAGE_FLAGS::EARLY_FRAGMENT_TESTS_BIT | PIPELINE_STAGE_FLAGS::COLOR_ATTACHMENT_OUTPUT_BIT, + // because depth and color get cleared first no read mask + .dstAccessMask = ACCESS_FLAGS::DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | ACCESS_FLAGS::COLOR_ATTACHMENT_WRITE_BIT + } + // leave view offsets and flags default + }, + // color from ATTACHMENT_OPTIMAL to PRESENT_SRC + { + .srcSubpass = 0, + .dstSubpass = IGPURenderpass::SCreationParams::SSubpassDependency::External, + .memoryBarrier = { + // last place where the color can get modified, depth is implicitly earlier + .srcStageMask = PIPELINE_STAGE_FLAGS::COLOR_ATTACHMENT_OUTPUT_BIT, + // only write ops, reads can't be made available + .srcAccessMask = ACCESS_FLAGS::COLOR_ATTACHMENT_WRITE_BIT + // spec says nothing is needed when presentation is the destination + } + // leave view offsets and flags default + }, + IGPURenderpass::SCreationParams::DependenciesEnd + }; + return dependencies; +} + +[[noreturn]] void MeshLoadersApp::failExit(const char* msg, ...) +{ + char formatted[4096] = {}; + va_list args; + va_start(args, msg); + vsnprintf(formatted, sizeof(formatted), msg, args); + va_end(args); + if (m_logger) + m_logger->log("%s", ILogger::ELL_ERROR, formatted); + std::exit(-1); +} + +bool MeshLoadersApp::initTestCases() +{ + m_cases.clear(); + m_caseNameCounts.clear(); + if (m_runMode == RunMode::Interactive) + { + system::path picked; + if (!pickModelPath(picked)) + return logFail("No file selected."); + m_cases.push_back({ makeUniqueCaseName(picked), picked }); + return true; + } + return loadTestList(m_testListPath); +} + +bool MeshLoadersApp::pickModelPath(system::path& outPath) +{ + if (m_fileDialogOpen) + { + if (m_logger) + m_logger->log("File dialog is already open. Ignoring request.", ILogger::ELL_WARNING); + return false; + } + + struct DialogGuard + { + bool& flag; + ~DialogGuard() { flag = false; } + }; + + m_fileDialogOpen = true; + DialogGuard guard{m_fileDialogOpen}; + + pfd::open_file file( + "Choose a supported Model File", + sharedInputCWD.string(), + { + "All Supported Formats", "*.ply *.stl *.serialized *.obj", + "Polygon File Format (.ply)", "*.ply", + "Stereolithography (.stl)", "*.stl", + "Mitsuba 0.6 Serialized (.serialized)", "*.serialized", + "Wavefront Object (.obj)", "*.obj" + }, + false); + + const auto selected = file.result(); + if (selected.empty()) + return false; + outPath = selected[0]; + return true; +} + +bool MeshLoadersApp::loadTestList(const system::path& jsonPath) +{ + if (!std::filesystem::exists(jsonPath)) + return logFail("Missing test list: %s", jsonPath.string().c_str()); + + std::ifstream stream(jsonPath); + if (!stream.is_open()) + return logFail("Failed to open test list: %s", jsonPath.string().c_str()); + + nlohmann::json doc; + try + { + stream >> doc; + } + catch (const std::exception& e) + { + return logFail("Invalid JSON in test list: %s", e.what()); + } + + if (!doc.contains("cases") || !doc["cases"].is_array()) + return logFail("Test list JSON missing \"cases\" array."); + + m_caseNameCounts.clear(); + + if (doc.contains("row_view")) + { + if (!doc["row_view"].is_boolean()) + return logFail("\"row_view\" must be a boolean."); + m_rowViewEnabled = doc["row_view"].get(); + } + + const auto baseDir = jsonPath.parent_path(); + for (const auto& entry : doc["cases"]) + { + std::string pathString; + + if (entry.is_string()) + { + pathString = entry.get(); + } + else if (entry.is_object()) + { + if (!entry.contains("path") || !entry["path"].is_string()) + return logFail("Test list entry missing \"path\"."); + pathString = entry["path"].get(); + } + else + return logFail("Invalid test list entry."); + + system::path path = pathString; + if (path.is_relative()) + path = baseDir / path; + if (!std::filesystem::exists(path)) + return logFail("Missing test input: %s", path.string().c_str()); + + m_cases.push_back({ makeUniqueCaseName(path), path }); + } + + if (m_cases.empty()) + return logFail("No test cases in test list."); + + return true; +} + +bool MeshLoadersApp::isRowViewActive() const +{ + return m_rowViewEnabled && m_runMode != RunMode::CI && m_runMode != RunMode::Interactive; +} + +std::string MeshLoadersApp::normalizeExtension(const system::path& path) +{ + auto ext = path.extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + return ext; +} + +bool MeshLoadersApp::isWriteExtensionSupported(const std::string& ext) const +{ + if (ext == ".ply" || ext == ".stl") + return true; +#ifdef _NBL_COMPILE_WITH_OBJ_WRITER_ + if (ext == ".obj") + return true; +#endif + return false; +} + +system::path MeshLoadersApp::resolveSavePath(const system::path& modelPath) const +{ + if (m_specifiedGeomSavePath) + return path(*m_specifiedGeomSavePath); + const auto stem = modelPath.stem().string(); + auto ext = normalizeExtension(modelPath); + if (ext.empty()) + ext = ".ply"; + if (!isWriteExtensionSupported(ext)) + { + if (m_logger) + m_logger->log("No writer for %s, writing .ply instead.", ILogger::ELL_WARNING, ext.c_str()); + ext = ".ply"; + } + return m_saveGeomPrefixPath / (stem + "_written" + ext); +} + +std::string MeshLoadersApp::sanitizeCaseNameForFilename(std::string name) +{ + for (auto& ch : name) + { + const unsigned char uch = static_cast(ch); + if (!(std::isalnum(uch) || ch == '_' || ch == '-' || ch == '.')) + ch = '_'; + } + if (name.empty()) + name = "unnamed_case"; + return name; +} + +system::path MeshLoadersApp::getGeometryHashReferencePath(const std::string& caseName) const +{ + return m_geometryHashReferenceDir / (sanitizeCaseNameForFilename(caseName) + ".geomhash"); +} + +std::string MeshLoadersApp::geometryHashToHex(const core::blake3_hash_t& hash) +{ + static constexpr char HexDigits[] = "0123456789abcdef"; + std::string out; + out.resize(sizeof(hash.data) * 2ull); + for (size_t i = 0ull; i < sizeof(hash.data); ++i) + { + const uint8_t v = hash.data[i]; + out[2ull * i + 0ull] = HexDigits[(v >> 4) & 0xfu]; + out[2ull * i + 1ull] = HexDigits[v & 0xfu]; + } + return out; +} + +bool MeshLoadersApp::tryParseNibble(const char c, uint8_t& out) +{ + if (c >= '0' && c <= '9') + { + out = static_cast(c - '0'); + return true; + } + if (c >= 'a' && c <= 'f') + { + out = static_cast(10 + c - 'a'); + return true; + } + if (c >= 'A' && c <= 'F') + { + out = static_cast(10 + c - 'A'); + return true; + } + return false; +} + +bool MeshLoadersApp::tryParseGeometryHashHex(std::string hex, core::blake3_hash_t& outHash) +{ + hex.erase(std::remove_if(hex.begin(), hex.end(), [](unsigned char c) { return std::isspace(c) != 0; }), hex.end()); + if (hex.size() != sizeof(outHash.data) * 2ull) + return false; + + for (size_t i = 0ull; i < sizeof(outHash.data); ++i) + { + uint8_t hi = 0u; + uint8_t lo = 0u; + if (!tryParseNibble(hex[2ull * i + 0ull], hi) || !tryParseNibble(hex[2ull * i + 1ull], lo)) + return false; + outHash.data[i] = static_cast((hi << 4) | lo); + } + return true; +} + +bool MeshLoadersApp::readGeometryHashReference(const system::path& refPath, core::blake3_hash_t& outHash) const +{ + std::ifstream in(refPath); + if (!in.is_open()) + return false; + std::string line; + std::getline(in, line); + return tryParseGeometryHashHex(std::move(line), outHash); +} + +bool MeshLoadersApp::writeGeometryHashReference(const system::path& refPath, const core::blake3_hash_t& hash) const +{ + std::error_code ec; + std::filesystem::create_directories(refPath.parent_path(), ec); + if (ec) + return false; + std::ofstream out(refPath, std::ios::binary | std::ios::trunc); + if (!out.is_open()) + return false; + out << geometryHashToHex(hash) << '\n'; + return out.good(); +} + +bool MeshLoadersApp::startCase(const size_t index) +{ + if (index >= m_cases.size()) + return false; + + m_caseIndex = index; + m_phase = Phase::RenderOriginal; + m_phaseFrameCounter = 0u; + m_loadedScreenshot = nullptr; + m_writtenScreenshot = nullptr; + m_referenceCamera.reset(); + m_hasReferenceGeometryHash = false; + m_caseGeometryHashReferencePath.clear(); + + const auto& testCase = m_cases[m_caseIndex]; + m_caseName = testCase.name.empty() ? testCase.path.stem().string() : testCase.name; + m_writtenPath = resolveSavePath(testCase.path); + m_loadedScreenshotPath = m_screenshotPrefixPath / ("meshloaders_" + m_caseName + "_loaded.png"); + m_writtenScreenshotPath = m_screenshotPrefixPath / ("meshloaders_" + m_caseName + "_written.png"); + + if (!loadModel(testCase.path, true, true)) + return false; + + if (m_currentCpuGeom) + { + const auto loadedGeometryHash = hashGeometry(m_currentCpuGeom.get()); + m_referenceGeometryHash = loadedGeometryHash; + m_hasReferenceGeometryHash = true; + m_caseGeometryHashReferencePath = getGeometryHashReferencePath(m_caseName); + + if (m_updateGeometryHashReferences) + { + const bool referenceExisted = std::filesystem::exists(m_caseGeometryHashReferencePath); + if (!writeGeometryHashReference(m_caseGeometryHashReferencePath, loadedGeometryHash)) + return logFail("Failed to write geometry hash reference: %s", m_caseGeometryHashReferencePath.string().c_str()); + if (!referenceExisted) + m_logger->log("Geometry hash reference did not exist for %s. Created new reference at %s", ILogger::ELL_WARNING, m_caseName.c_str(), m_caseGeometryHashReferencePath.string().c_str()); + else + m_logger->log("Geometry hash reference updated for %s at %s", ILogger::ELL_INFO, m_caseName.c_str(), m_caseGeometryHashReferencePath.string().c_str()); + } + else if (m_runMode == RunMode::CI) + { + if (!std::filesystem::exists(m_caseGeometryHashReferencePath)) + return logFail("Missing geometry hash reference for %s at %s. Run once with --update-references.", m_caseName.c_str(), m_caseGeometryHashReferencePath.string().c_str()); + + core::blake3_hash_t onDiskHash = {}; + if (!readGeometryHashReference(m_caseGeometryHashReferencePath, onDiskHash)) + return logFail("Invalid geometry hash reference for %s at %s", m_caseName.c_str(), m_caseGeometryHashReferencePath.string().c_str()); + + m_referenceGeometryHash = onDiskHash; + m_hasReferenceGeometryHash = true; + if (loadedGeometryHash != onDiskHash) + { + m_logger->log("Loaded geometry hash mismatch for %s. Current=%s Reference=%s", ILogger::ELL_ERROR, m_caseName.c_str(), geometryHashToHex(loadedGeometryHash).c_str(), geometryHashToHex(onDiskHash).c_str()); + return logFail("Loaded asset differs from stored geometry hash reference for %s.", m_caseName.c_str()); + } + } + } + + return true; +} + +bool MeshLoadersApp::advanceToNextCase() +{ + const auto nextIndex = m_caseIndex + 1u; + if (nextIndex >= m_cases.size()) + { + m_shouldQuit = true; + return false; + } + if (!startCase(nextIndex)) + { + m_shouldQuit = true; + return false; + } + return true; +} + +void MeshLoadersApp::reloadInteractive() +{ + system::path picked; + if (!pickModelPath(picked)) + failExit("No file selected."); + if (!loadModel(picked, true, true)) + failExit("Failed to load asset %s.", picked.string().c_str()); + if (m_currentCpuGeom && m_saveGeom) + { + const auto savePath = resolveSavePath(picked); + if (!writeGeometry(m_currentCpuGeom, savePath.string())) + failExit("Geometry write failed."); + } +} + +bool MeshLoadersApp::addRowViewCase() +{ + system::path picked; + if (!pickModelPath(picked)) + return false; + return addRowViewCaseFromPath(picked); +} + +bool MeshLoadersApp::addRowViewCaseFromPath(const system::path& picked) +{ + if (picked.empty()) + return false; + m_cases.push_back({ makeUniqueCaseName(picked), picked }); + m_shouldQuit = false; + return loadRowView(RowViewReloadMode::Incremental); +} + +bool MeshLoadersApp::reloadFromTestList() +{ + m_cases.clear(); + if (!loadTestList(m_testListPath)) + return false; + m_shouldQuit = false; + m_rowViewScreenshotCaptured = false; + if (isRowViewActive()) + { + m_nonInteractiveTest = false; + return loadRowView(RowViewReloadMode::Full); + } + m_nonInteractiveTest = (m_runMode != RunMode::Interactive); + return startCase(0u); +} + + + diff --git a/12_MeshLoaders/MeshLoadersAppLoad.cpp b/12_MeshLoaders/MeshLoadersAppLoad.cpp new file mode 100644 index 000000000..24f1ce29a --- /dev/null +++ b/12_MeshLoaders/MeshLoadersAppLoad.cpp @@ -0,0 +1,799 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#include "MeshLoadersApp.hpp" + +#include +#include + +#include + +bool MeshLoadersApp::loadModel(const system::path& modelPath, bool updateCamera, bool storeCamera) +{ + if (modelPath.empty()) + failExit("Empty model path."); + if (!std::filesystem::exists(modelPath)) + failExit("Missing input: %s", modelPath.string().c_str()); + using clock_t = std::chrono::high_resolution_clock; + const auto loadOuterStart = clock_t::now(); + + m_modelPath = modelPath.string(); + + // free up + m_renderer->m_instances.clear(); + m_renderer->clearGeometries({ .semaphore = m_semaphore.get(),.value = m_realFrameIx }); + m_assetMgr->clearAllAssetCache(); + + //! load the geometry + IAssetLoader::SAssetLoadParams params = makeLoadParams(); + AssetLoadCallResult loadResult = {}; + if (!loadAssetCallFromPath(modelPath, params, loadResult)) + failExit("Failed to open input file %s.", modelPath.string().c_str()); + const auto loadMs = loadResult.getAssetMs; + auto asset = std::move(loadResult.bundle); + m_logger->log( + "Asset load call perf: path=%s time=%.3f ms size=%llu", + ILogger::ELL_INFO, + m_modelPath.c_str(), + loadMs, + static_cast(loadResult.inputSize)); + if (asset.getContents().empty()) + failExit("Failed to load asset %s.", m_modelPath.c_str()); + + core::vector> geometries; + const auto extractStart = clock_t::now(); + if (!appendGeometriesFromBundle(asset, geometries)) + failExit("Asset loaded but not a supported type for %s.", m_modelPath.c_str()); + const auto extractMs = toMs(clock_t::now() - extractStart); + if (geometries.empty()) + failExit("No geometry found in asset %s.", m_modelPath.c_str()); + const auto outerMs = toMs(clock_t::now() - loadOuterStart); + const auto nonLoaderMs = std::max(0.0, outerMs - loadMs); + m_logger->log( + "Asset load outer perf: path=%s getAsset=%.3f ms extract=%.3f ms total=%.3f ms non_loader=%.3f ms", + ILogger::ELL_INFO, + m_modelPath.c_str(), + loadMs, + extractMs, + outerMs, + nonLoaderMs); + + m_currentCpuGeom = geometries[0]; + + using aabb_t = hlsl::shapes::AABB<3, double>; + auto printAABB = [&](const aabb_t& aabb, const char* extraMsg = "")->void + { + m_logger->log("%s AABB is (%f,%f,%f) -> (%f,%f,%f)", ILogger::ELL_INFO, extraMsg, aabb.minVx.x, aabb.minVx.y, aabb.minVx.z, aabb.maxVx.x, aabb.maxVx.y, aabb.maxVx.z); + }; + auto bound = aabb_t::create(); + // convert the geometries + { + smart_refctd_ptr converter = CAssetConverter::create({ .device = m_device.get() }); + + const auto transferFamily = getTransferUpQueue()->getFamilyIndex(); + + struct SInputs : CAssetConverter::SInputs + { + virtual inline std::span getSharedOwnershipQueueFamilies(const size_t groupCopyID, const asset::ICPUBuffer* buffer, const CAssetConverter::patch_t& patch) const + { + return sharedBufferOwnership; + } + + core::vector sharedBufferOwnership; + } inputs = {}; + core::vector> patches(geometries.size(), CSimpleDebugRenderer::DefaultPolygonGeometryPatch); + { + inputs.logger = m_logger.get(); + std::get>(inputs.assets) = { &geometries.front().get(),geometries.size() }; + std::get>(inputs.patches) = patches; + // set up shared ownership so we don't have to + core::unordered_set families; + families.insert(transferFamily); + families.insert(getGraphicsQueue()->getFamilyIndex()); + if (families.size() > 1) + for (const auto fam : families) + inputs.sharedBufferOwnership.push_back(fam); + } + + // reserve + auto reservation = converter->reserve(inputs); + if (!reservation) + { + failExit("Failed to reserve GPU objects for CPU->GPU conversion."); + } + + // convert + { + auto semaphore = m_device->createSemaphore(0u); + + constexpr auto MultiBuffering = 2; + std::array, MultiBuffering> commandBuffers = {}; + { + auto pool = m_device->createCommandPool(transferFamily, IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT | IGPUCommandPool::CREATE_FLAGS::TRANSIENT_BIT); + pool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, commandBuffers, smart_refctd_ptr(m_logger)); + } + commandBuffers.front()->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); + + std::array commandBufferSubmits; + for (auto i = 0; i < MultiBuffering; i++) + commandBufferSubmits[i].cmdbuf = commandBuffers[i].get(); + + SIntendedSubmitInfo transfer = {}; + transfer.queue = getTransferUpQueue(); + transfer.scratchCommandBuffers = commandBufferSubmits; + transfer.scratchSemaphore = { + .semaphore = semaphore.get(), + .value = 0u, + .stageMask = PIPELINE_STAGE_FLAGS::ALL_TRANSFER_BITS + }; + + CAssetConverter::SConvertParams cpar = {}; + cpar.utilities = m_utils.get(); + cpar.transfer = &transfer; + + auto future = reservation.convert(cpar); + if (future.copy() != IQueue::RESULT::SUCCESS) + failExit("Failed to await submission feature."); + } + + auto tmp = hlsl::float32_t4x3( + hlsl::float32_t3(1, 0, 0), + hlsl::float32_t3(0, 1, 0), + hlsl::float32_t3(0, 0, 1), + hlsl::float32_t3(0, 0, 0)); + core::vector worldTforms; + const auto& converted = reservation.getGPUObjects(); + m_aabbInstances.resize(converted.size()); + if (m_drawBBMode == DBBM_OBB) + m_obbInstances.resize(converted.size()); + for (uint32_t i = 0; i < converted.size(); i++) + { + const auto& cpuGeom = geometries[i].get(); + const auto promoted = getGeometryAABB(cpuGeom); + printAABB(promoted, "Geometry"); + const auto promotedWorld = hlsl::float64_t3x4(worldTforms.emplace_back(hlsl::transpose(tmp))); + const auto translation = hlsl::float64_t3( + static_cast(tmp[3].x), + static_cast(tmp[3].y), + static_cast(tmp[3].z)); + const auto transformed = translateAABB(promoted, translation); + printAABB(transformed, "Transformed"); + bound = hlsl::shapes::util::union_(transformed, bound); + +#ifdef NBL_BUILD_DEBUG_DRAW + auto& aabbInst = m_aabbInstances[i]; + const auto tmpAabb = shapes::AABB<3, float>(promoted.minVx, promoted.maxVx); + + hlsl::float32_t3x4 aabbTransform = ext::debug_draw::DrawAABB::getTransformFromAABB(tmpAabb); + const auto tmpWorld = hlsl::float32_t3x4(promotedWorld); + const auto world4x4 = float32_t4x4{ + tmpWorld[0], + tmpWorld[1], + tmpWorld[2], + float32_t4(0, 0, 0, 1) + }; + + aabbInst.color = { 1, 1, 1, 1 }; + aabbInst.transform = math::linalg::promoted_mul(world4x4, aabbTransform); + + if (m_drawBBMode == DBBM_OBB) + { + auto& obbInst = m_obbInstances[i]; + const auto obb = CPolygonGeometryManipulator::calculateOBB( + cpuGeom->getPositionView().getElementCount(), + [geo = cpuGeom, &world4x4](size_t vertex_i) { + hlsl::float32_t3 pt; + geo->getPositionView().decodeElement(vertex_i, pt); + return pt; + }); + obbInst.color = { 0, 0, 1, 1 }; + obbInst.transform = math::linalg::promoted_mul(world4x4, obb.transform); + } +#endif + } + + printAABB(bound, "Total"); + if (!m_renderer->addGeometries({ &converted.front().get(),converted.size() })) + failExit("Failed to add geometries to renderer."); + if (m_logger) + { + const auto& gpuGeos = m_renderer->getGeometries(); + for (size_t geoIx = 0u; geoIx < gpuGeos.size(); ++geoIx) + { + const auto& gpuGeo = gpuGeos[geoIx]; + m_logger->log( + "Renderer geo state: idx=%llu elem=%u posView=%u normalView=%u indexType=%u", + ILogger::ELL_DEBUG, + static_cast(geoIx), + gpuGeo.elementCount, + static_cast(gpuGeo.positionView), + static_cast(gpuGeo.normalView), + static_cast(gpuGeo.indexType)); + } + } + + auto worlTformsIt = worldTforms.begin(); + for (const auto& geo : m_renderer->getGeometries()) + m_renderer->m_instances.push_back({ + .world = *(worlTformsIt++), + .packedGeo = &geo + }); + } + + if (updateCamera) + { + setupCameraFromAABB(bound); + if (storeCamera) + storeCameraState(); + } + else if (m_referenceCamera) + applyCameraState(*m_referenceCamera); + else + setupCameraFromAABB(bound); + + return true; +} + +bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) +{ + if (m_cases.empty()) + failExit("No test cases loaded for row view."); + + using clock_t = std::chrono::high_resolution_clock; + RowViewPerfStats stats = {}; + stats.incremental = (mode == RowViewReloadMode::Incremental); + stats.cases = m_cases.size(); + const auto totalStart = clock_t::now(); + + const auto clearStart = clock_t::now(); + if (mode == RowViewReloadMode::Full) + { + m_renderer->m_instances.clear(); + m_renderer->clearGeometries({ .semaphore = m_semaphore.get(),.value = m_realFrameIx }); + } + stats.clearMs = toMs(clock_t::now() - clearStart); + + core::vector> geometries; + core::vector> aabbs; + geometries.reserve(m_cases.size()); + aabbs.reserve(m_cases.size()); + + core::vector> cpuToConvert; + core::vector convertEntries; + + m_rowViewCache.reserve(m_cases.size()); + + IAssetLoader::SAssetLoadParams params = makeLoadParams(); + + for (const auto& testCase : m_cases) + { + const auto& path = testCase.path; + if (!std::filesystem::exists(path)) + failExit("Missing input: %s", path.string().c_str()); + + const auto cacheKey = makeCacheKey(path); + auto& entry = m_rowViewCache[cacheKey]; + double assetLoadMs = 0.0; + bool cached = true; + if (!entry.cpu) + { + stats.cpuMisses++; + cached = false; + AssetLoadCallResult loadResult = {}; + if (!loadAssetCallFromPath(path, params, loadResult)) + failExit("Failed to open input file %s.", path.string().c_str()); + auto asset = std::move(loadResult.bundle); + assetLoadMs = loadResult.getAssetMs; + stats.loadMs += assetLoadMs; + if (asset.getContents().empty()) + failExit("Failed to load asset %s.", path.string().c_str()); + + const auto extractStart = clock_t::now(); + core::vector> found; + if (appendGeometriesFromBundle(asset, found)) + { + if (!found.empty()) + entry.cpu = found.front(); + } + stats.extractMs += toMs(clock_t::now() - extractStart); + if (!entry.cpu) + failExit("No geometry found in asset %s.", path.string().c_str()); + + const auto aabbStart = clock_t::now(); + entry.aabb = getGeometryAABB(entry.cpu.get()); + entry.hasAabb = isValidAABB(entry.aabb); + stats.aabbMs += toMs(clock_t::now() - aabbStart); + } + else + { + stats.cpuHits++; + if (!entry.hasAabb) + { + const auto aabbStart = clock_t::now(); + entry.aabb = getGeometryAABB(entry.cpu.get()); + entry.hasAabb = isValidAABB(entry.aabb); + stats.aabbMs += toMs(clock_t::now() - aabbStart); + } + } + logRowViewAssetLoad(path, assetLoadMs, cached); + + if (!entry.gpu) + { + stats.gpuMisses++; + cpuToConvert.push_back(entry.cpu); + convertEntries.push_back(&entry); + } + else + { + stats.gpuHits++; + } + + geometries.push_back(entry.cpu); + aabbs.push_back(entry.aabb); + } + + if (geometries.empty()) + failExit("No geometry found for row view."); + logRowViewLoadTotal(stats.loadMs, stats.cpuHits, stats.cpuMisses); + + if (!cpuToConvert.empty()) + { + stats.convertCount = cpuToConvert.size(); + const auto convertStart = clock_t::now(); + + smart_refctd_ptr converter = CAssetConverter::create({ .device = m_device.get() }); + const auto transferFamily = getTransferUpQueue()->getFamilyIndex(); + + struct SInputs : CAssetConverter::SInputs + { + virtual inline std::span getSharedOwnershipQueueFamilies(const size_t, const asset::ICPUBuffer*, const CAssetConverter::patch_t&) const + { + return sharedBufferOwnership; + } + + core::vector sharedBufferOwnership; + } inputs = {}; + core::vector> patches(cpuToConvert.size(), CSimpleDebugRenderer::DefaultPolygonGeometryPatch); + { + inputs.logger = m_logger.get(); + std::get>(inputs.assets) = { &cpuToConvert.front().get(),cpuToConvert.size() }; + std::get>(inputs.patches) = patches; + core::unordered_set families; + families.insert(transferFamily); + families.insert(getGraphicsQueue()->getFamilyIndex()); + if (families.size() > 1) + for (const auto fam : families) + inputs.sharedBufferOwnership.push_back(fam); + } + + auto reservation = converter->reserve(inputs); + if (!reservation) + failExit("Failed to reserve GPU objects for CPU->GPU conversion."); + + { + auto semaphore = m_device->createSemaphore(0u); + + constexpr auto MultiBuffering = 2; + std::array, MultiBuffering> commandBuffers = {}; + { + auto pool = m_device->createCommandPool(transferFamily, IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT | IGPUCommandPool::CREATE_FLAGS::TRANSIENT_BIT); + pool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, commandBuffers, smart_refctd_ptr(m_logger)); + } + commandBuffers.front()->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); + + std::array commandBufferSubmits; + for (auto i = 0; i < MultiBuffering; i++) + commandBufferSubmits[i].cmdbuf = commandBuffers[i].get(); + + SIntendedSubmitInfo transfer = {}; + transfer.queue = getTransferUpQueue(); + transfer.scratchCommandBuffers = commandBufferSubmits; + transfer.scratchSemaphore = { + .semaphore = semaphore.get(), + .value = 0u, + .stageMask = PIPELINE_STAGE_FLAGS::ALL_TRANSFER_BITS + }; + + CAssetConverter::SConvertParams cpar = {}; + cpar.utilities = m_utils.get(); + cpar.transfer = &transfer; + + auto future = reservation.convert(cpar); + if (future.copy() != IQueue::RESULT::SUCCESS) + failExit("Failed to await submission feature."); + } + + const auto& converted = reservation.getGPUObjects(); + for (size_t i = 0u; i < converted.size(); ++i) + convertEntries[i]->gpu = converted[i]; + + stats.convertMs = toMs(clock_t::now() - convertStart); + } + + size_t existingCount = m_renderer->getGeometries().size(); + const bool incremental = (mode == RowViewReloadMode::Incremental) && (existingCount <= m_cases.size()); + if (!incremental && mode == RowViewReloadMode::Incremental) + return loadRowView(RowViewReloadMode::Full); + + if (mode == RowViewReloadMode::Full) + { + core::vector allGeometries; + allGeometries.reserve(m_cases.size()); + for (const auto& testCase : m_cases) + { + const auto& entry = m_rowViewCache[makeCacheKey(testCase.path)]; + if (!entry.gpu) + failExit("Missing GPU geometry for %s.", testCase.path.string().c_str()); + allGeometries.push_back(entry.gpu.get()); + } + stats.addCount = allGeometries.size(); + const auto addStart = clock_t::now(); + if (!allGeometries.empty()) + if (!m_renderer->addGeometries({ allGeometries.data(),allGeometries.size() })) + failExit("Failed to add geometries to renderer."); + stats.addGeoMs = toMs(clock_t::now() - addStart); + } + else + { + const size_t addCount = (existingCount < m_cases.size()) ? (m_cases.size() - existingCount) : 0u; + stats.addCount = addCount; + if (addCount > 0u) + { + core::vector newGeometries; + newGeometries.reserve(addCount); + for (size_t i = existingCount; i < m_cases.size(); ++i) + { + const auto& entry = m_rowViewCache[makeCacheKey(m_cases[i].path)]; + if (!entry.gpu) + failExit("Missing GPU geometry for %s.", m_cases[i].path.string().c_str()); + newGeometries.push_back(entry.gpu.get()); + } + const auto addStart = clock_t::now(); + if (!m_renderer->addGeometries({ newGeometries.data(),newGeometries.size() })) + failExit("Failed to add geometries to renderer."); + stats.addGeoMs = toMs(clock_t::now() - addStart); + } + } + + using aabb_t = hlsl::shapes::AABB<3, double>; + auto printAABB = [&](const aabb_t& aabb, const char* extraMsg = "")->void + { + m_logger->log("%s AABB is (%f,%f,%f) -> (%f,%f,%f)", ILogger::ELL_INFO, extraMsg, aabb.minVx.x, aabb.minVx.y, aabb.minVx.z, aabb.maxVx.x, aabb.maxVx.y, aabb.maxVx.z); + }; + auto bound = aabb_t::create(); + + const auto layoutStart = clock_t::now(); + double targetExtent = 0.0; + core::vector maxDims; + maxDims.reserve(aabbs.size()); + for (const auto& aabb : aabbs) + { + const auto extent = aabb.getExtent(); + const double maxDim = std::max({ extent.x, extent.y, extent.z, 0.001 }); + maxDims.push_back(maxDim); + if (maxDim > targetExtent) + targetExtent = maxDim; + } + + core::vector scales; + scales.reserve(aabbs.size()); + for (const auto maxDim : maxDims) + scales.push_back(targetExtent / maxDim); + + double maxWidth = 0.0; + double totalWidth = 0.0; + core::vector widths; + widths.reserve(aabbs.size()); + for (size_t i = 0; i < aabbs.size(); ++i) + { + const auto extent = aabbs[i].getExtent(); + const double width = std::max(0.001, extent.x * scales[i]); + widths.push_back(width); + totalWidth += width; + if (width > maxWidth) + maxWidth = width; + } + const double spacing = std::max(0.05 * maxWidth, 0.01); + const double totalSpan = totalWidth + spacing * double(widths.size() > 0 ? widths.size() - 1 : 0); + double cursor = -0.5 * totalSpan; + stats.layoutMs = toMs(clock_t::now() - layoutStart); + + const auto instanceStart = clock_t::now(); + auto tmp = hlsl::float32_t4x3( + hlsl::float32_t3(1, 0, 0), + hlsl::float32_t3(0, 1, 0), + hlsl::float32_t3(0, 0, 1), + hlsl::float32_t3(0, 0, 0) + ); + core::vector worldTforms; + worldTforms.reserve(geometries.size()); + m_aabbInstances.resize(geometries.size()); + if (m_drawBBMode == DBBM_OBB) + m_obbInstances.resize(geometries.size()); + m_renderer->m_instances.clear(); + + for (uint32_t i = 0; i < geometries.size(); i++) + { + const auto& cpuGeom = geometries[i].get(); + const auto aabb = aabbs[i]; + printAABB(aabb, "Geometry"); + + const double scale = scales[i]; + const auto center = (aabb.minVx + aabb.maxVx) * 0.5; + const double width = widths[i]; + const double targetCenterX = cursor + 0.5 * width; + cursor += width + spacing; + + const double tx = targetCenterX - scale * center.x; + const double ty = -scale * center.y; + const double tz = -scale * center.z; + tmp[0] = hlsl::float32_t3(static_cast(scale), 0.f, 0.f); + tmp[1] = hlsl::float32_t3(0.f, static_cast(scale), 0.f); + tmp[2] = hlsl::float32_t3(0.f, 0.f, static_cast(scale)); + tmp[3] = hlsl::float32_t3(static_cast(tx), static_cast(ty), static_cast(tz)); + + const auto promotedWorld = hlsl::float64_t3x4(worldTforms.emplace_back(hlsl::transpose(tmp))); + const auto translation = hlsl::float64_t3(tx, ty, tz); + const auto scaled = scaleAABB(aabb, scale); + const auto transformed = translateAABB(scaled, translation); + printAABB(transformed, "Transformed"); + bound = hlsl::shapes::util::union_(transformed, bound); + +#ifdef NBL_BUILD_DEBUG_DRAW + auto& aabbInst = m_aabbInstances[i]; + const auto tmpAabb = shapes::AABB<3, float>(aabb.minVx, aabb.maxVx); + hlsl::float32_t3x4 aabbTransform = ext::debug_draw::DrawAABB::getTransformFromAABB(tmpAabb); + const auto tmpWorld = hlsl::float32_t3x4(promotedWorld); + const auto world4x4 = float32_t4x4{ + tmpWorld[0], + tmpWorld[1], + tmpWorld[2], + float32_t4(0, 0, 0, 1) + }; + aabbInst.color = { 1,1,1,1 }; + aabbInst.transform = math::linalg::promoted_mul(world4x4, aabbTransform); + + if (m_drawBBMode == DBBM_OBB) + { + auto& obbInst = m_obbInstances[i]; + const auto obb = CPolygonGeometryManipulator::calculateOBB( + cpuGeom->getPositionView().getElementCount(), + [geo = cpuGeom](size_t vertex_i) { + hlsl::float32_t3 pt; + geo->getPositionView().decodeElement(vertex_i, pt); + return pt; + }); + obbInst.color = { 0, 0, 1, 1 }; + obbInst.transform = math::linalg::promoted_mul(world4x4, obb.transform); + } +#endif + } + + printAABB(bound, "Total"); + for (uint32_t i = 0; i < worldTforms.size(); i++) + { + m_renderer->m_instances.push_back({ + .world = worldTforms[i], + .packedGeo = &m_renderer->getGeometry(i) + }); + } + stats.instanceMs = toMs(clock_t::now() - instanceStart); + + const auto cameraStart = clock_t::now(); + setupCameraFromAABB(bound); + stats.cameraMs = toMs(clock_t::now() - cameraStart); + + m_modelPath = "Row view (all meshes)"; + m_rowViewScreenshotPath = m_screenshotPrefixPath / "meshloaders_row_view.png"; + m_rowViewScreenshotCaptured = false; + stats.totalMs = toMs(clock_t::now() - totalStart); + logRowViewPerf(stats); + return true; +} + +bool MeshLoadersApp::writeGeometry(smart_refctd_ptr geometry, const std::string& savePath) +{ + using clock_t = std::chrono::high_resolution_clock; + const auto writeOuterStart = clock_t::now(); + IAsset* assetPtr = const_cast(static_cast(geometry.get())); + const auto ext = normalizeExtension(system::path(savePath)); + auto flags = asset::EWF_MESH_IS_RIGHT_HANDED; + if (ext != ".obj") + flags = static_cast(flags | asset::EWF_BINARY); + IAssetWriter::SAssetWriteParams params{ assetPtr, flags }; + params.logger = getAssetLoadLogger(); + m_logger->log("Saving mesh to %s", ILogger::ELL_INFO, savePath.c_str()); + const auto openStart = clock_t::now(); + system::ISystem::future_t> writeFileFuture; + m_system->createFile(writeFileFuture, system::path(savePath), system::IFile::ECF_WRITE); + core::smart_refctd_ptr writeFile; + writeFileFuture.acquire().move_into(writeFile); + const auto openMs = toMs(clock_t::now() - openStart); + if (!writeFile) + { + m_logger->log("Failed to open output file %s", ILogger::ELL_ERROR, savePath.c_str()); + return false; + } + const auto start = clock_t::now(); + if (!m_assetMgr->writeAsset(writeFile.get(), params)) + { + const auto ms = toMs(clock_t::now() - start); + m_logger->log("Failed to save %s after %.3f ms", ILogger::ELL_ERROR, savePath.c_str(), ms); + return false; + } + const auto writeMs = toMs(clock_t::now() - start); + const auto statStart = clock_t::now(); + uintmax_t size = 0u; + if (std::filesystem::exists(savePath)) + size = std::filesystem::file_size(savePath); + const auto statMs = toMs(clock_t::now() - statStart); + const auto outerMs = toMs(clock_t::now() - writeOuterStart); + const auto nonWriterMs = std::max(0.0, outerMs - writeMs); + m_logger->log("Asset write call perf: path=%s ext=%s time=%.3f ms size=%llu", ILogger::ELL_INFO, savePath.c_str(), ext.c_str(), writeMs, static_cast(size)); + m_logger->log( + "Asset write outer perf: path=%s ext=%s open=%.3f ms writeAsset=%.3f ms stat=%.3f ms total=%.3f ms non_writer=%.3f ms size=%llu", + ILogger::ELL_INFO, + savePath.c_str(), + ext.c_str(), + openMs, + writeMs, + statMs, + outerMs, + nonWriterMs, + static_cast(size)); + m_logger->log("Writer perf: path=%s ext=%s time=%.3f ms size=%llu", ILogger::ELL_INFO, savePath.c_str(), ext.c_str(), writeMs, static_cast(size)); + m_logger->log("Mesh successfully saved!", ILogger::ELL_INFO); + return true; +} + +void MeshLoadersApp::setupCameraFromAABB(const hlsl::shapes::AABB<3, double>& bound) +{ + const auto extent = bound.getExtent(); + const auto aspectRatio = double(m_window->getWidth()) / double(m_window->getHeight()); + const double fovY = 1.2; + const double fovX = 2.0 * std::atan(std::tan(fovY * 0.5) * aspectRatio); + const auto center = (bound.minVx + bound.maxVx) * 0.5; + const auto halfExtent = extent * 0.5; + const double halfX = std::max(halfExtent.x, 0.001); + const double halfY = std::max(halfExtent.y, 0.001); + const double halfZ = std::max(halfExtent.z, 0.001); + const double safeRadius = std::max({ halfX, halfY, halfZ }); + + const double distY = halfY / std::tan(fovY * 0.5); + const double distX = halfX / std::tan(fovX * 0.5); + double dist = std::max(distX, distY) + halfZ; + dist *= 1.1; + + const auto dir = hlsl::float64_t3(0.0, 0.0, 1.0); + const auto pos = center + dir * dist; + + const double margin = halfZ * 0.1 + 0.01; + const double nearPlane = std::max(0.001, dist - halfZ - margin); + const double farPlane = dist + halfZ + margin; + + const auto projection = nbl::hlsl::buildProjectionMatrixPerspectiveFovRH( + static_cast(fovY), + static_cast(aspectRatio), + static_cast(nearPlane), + static_cast(farPlane)); + camera.setProjectionMatrix(projection); + camera.setMoveSpeed(static_cast(safeRadius * 0.1)); + camera.setPosition(vectorSIMDf(pos.x, pos.y, pos.z)); + camera.setTarget(vectorSIMDf(center.x, center.y, center.z)); +} + +hlsl::shapes::AABB<3, double> MeshLoadersApp::translateAABB(const hlsl::shapes::AABB<3, double>& aabb, const hlsl::float64_t3& translation) +{ + auto out = aabb; + out.minVx += translation; + out.maxVx += translation; + return out; +} + +hlsl::shapes::AABB<3, double> MeshLoadersApp::scaleAABB(const hlsl::shapes::AABB<3, double>& aabb, const double scale) +{ + auto out = aabb; + out.minVx *= scale; + out.maxVx *= scale; + return out; +} + +void MeshLoadersApp::storeCameraState() +{ + m_referenceCamera = CameraState{ + camera.getPosition(), + camera.getTarget(), + camera.getProjectionMatrix(), + camera.getMoveSpeed() + }; +} + +void MeshLoadersApp::applyCameraState(const CameraState& state) +{ + camera.setProjectionMatrix(state.projection); + camera.setPosition(state.position); + camera.setTarget(state.target); + camera.setMoveSpeed(state.moveSpeed); +} + +bool MeshLoadersApp::isValidAABB(const hlsl::shapes::AABB<3, double>& aabb) +{ + return + (aabb.minVx.x <= aabb.maxVx.x) && + (aabb.minVx.y <= aabb.maxVx.y) && + (aabb.minVx.z <= aabb.maxVx.z); +} + +hlsl::shapes::AABB<3, double> MeshLoadersApp::getGeometryAABB(const ICPUPolygonGeometry* geometry) const +{ + if (!geometry) + return hlsl::shapes::AABB<3, double>::create(); + auto aabb = geometry->getAABB>(); + if (!isValidAABB(aabb)) + { + CPolygonGeometryManipulator::recomputeAABB(geometry); + aabb = geometry->getAABB>(); + } + return aabb; +} + +system::ILogger* MeshLoadersApp::getAssetLoadLogger() const +{ + if (m_assetLoadLogger) + return m_assetLoadLogger.get(); + return m_logger.get(); +} + +IAssetLoader::SAssetLoadParams MeshLoadersApp::makeLoadParams() const +{ + IAssetLoader::SAssetLoadParams params = {}; + params.logger = getAssetLoadLogger(); + if ((m_runMode == RunMode::CI || isRowViewActive()) && !m_loaderPerfLogger) + params.logger = nullptr; + params.cacheFlags = IAssetLoader::ECF_DUPLICATE_TOP_LEVEL; + params.ioPolicy.runtimeTuning.mode = m_runtimeTuningMode; + if (m_forceLoaderContentHashes) + params.loaderFlags = static_cast(params.loaderFlags | IAssetLoader::ELPF_COMPUTE_CONTENT_HASHES); + return params; +} + +bool MeshLoadersApp::loadAssetCallFromPath(const system::path& modelPath, const IAssetLoader::SAssetLoadParams& params, AssetLoadCallResult& out) +{ + using clock_t = std::chrono::high_resolution_clock; + if (std::filesystem::exists(modelPath)) + out.inputSize = std::filesystem::file_size(modelPath); + else + out.inputSize = 0u; + + const auto loadStart = clock_t::now(); + out.bundle = m_assetMgr->getAsset(modelPath.string(), params); + out.getAssetMs = toMs(clock_t::now() - loadStart); + return true; +} + +bool MeshLoadersApp::initLoaderPerfLogger(const system::path& logPath) +{ + if (!m_system) + return logFail("Could not initialize loader perf logger because system is unavailable."); + if (logPath.empty()) + return false; + const auto parent = logPath.parent_path(); + if (!parent.empty()) + { + std::error_code ec; + std::filesystem::create_directories(parent, ec); + if (ec) + return logFail("Could not create loader perf log directory %s", parent.string().c_str()); + } + system::ISystem::future_t> future; + m_system->createFile(future, logPath, system::IFile::ECF_READ_WRITE); + if (!future.wait() || !future.get()) + return logFail("Could not create loader perf log file %s", logPath.string().c_str()); + const auto logMask = core::bitflag(system::ILogger::ELL_ALL); + m_loaderPerfLogger = core::make_smart_refctd_ptr(future.copy(), false, logMask); + m_assetLoadLogger = m_loaderPerfLogger; + return true; +} + + diff --git a/12_MeshLoaders/MeshLoadersAppRuntime.cpp b/12_MeshLoaders/MeshLoadersAppRuntime.cpp new file mode 100644 index 000000000..362981020 --- /dev/null +++ b/12_MeshLoaders/MeshLoadersAppRuntime.cpp @@ -0,0 +1,314 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#include "MeshLoadersApp.hpp" + +#include "nbl/ext/ScreenShot/ScreenShot.h" + +std::string MeshLoadersApp::makeUniqueCaseName(const system::path& path) +{ + auto base = path.stem().string(); + if (base.empty()) + base = "case"; + auto& counter = m_caseNameCounts[base]; + std::string name = (counter == 0u) ? base : (base + "_" + std::to_string(counter)); + ++counter; + return name; +} + +double MeshLoadersApp::toMs(const std::chrono::high_resolution_clock::duration& d) +{ + return std::chrono::duration(d).count(); +} + +std::string MeshLoadersApp::makeCacheKey(const system::path& path) const +{ + return path.lexically_normal().generic_string(); +} + +void MeshLoadersApp::logRowViewPerf(const RowViewPerfStats& stats) const +{ + if (!m_logger) + return; + m_logger->log( + "RowView perf: mode=%s cases=%llu cpuHit=%llu cpuMiss=%llu gpuHit=%llu gpuMiss=%llu convert=%llu add=%llu total=%.3f ms", + ILogger::ELL_INFO, + stats.incremental ? "inc" : "full", + static_cast(stats.cases), + static_cast(stats.cpuHits), + static_cast(stats.cpuMisses), + static_cast(stats.gpuHits), + static_cast(stats.gpuMisses), + static_cast(stats.convertCount), + static_cast(stats.addCount), + stats.totalMs); + m_logger->log( + "RowView perf: clear=%.3f load=%.3f extract=%.3f aabb=%.3f convert=%.3f add=%.3f layout=%.3f inst=%.3f cam=%.3f", + ILogger::ELL_INFO, + stats.clearMs, + stats.loadMs, + stats.extractMs, + stats.aabbMs, + stats.convertMs, + stats.addGeoMs, + stats.layoutMs, + stats.instanceMs, + stats.cameraMs); +} + +void MeshLoadersApp::logRowViewAssetLoad(const system::path& path, const double ms, const bool cached) const +{ + if (!m_logger) + return; + m_logger->log( + "RowView perf: asset %s load=%.3f ms%s", + ILogger::ELL_INFO, + path.string().c_str(), + ms, + cached ? " (cached)" : ""); +} + +void MeshLoadersApp::logRowViewLoadTotal(const double ms, const size_t hits, const size_t misses) const +{ + if (!m_logger) + return; + m_logger->log( + "RowView perf: asset load total=%.3f ms hits=%llu misses=%llu", + ILogger::ELL_INFO, + ms, + static_cast(hits), + static_cast(misses)); +} + +core::blake3_hash_t MeshLoadersApp::hashGeometry(const ICPUPolygonGeometry* geo) +{ + return CPolygonGeometryManipulator::computeDeterministicContentHash(geo); +} + +bool MeshLoadersApp::validateWrittenAsset(const system::path& path) +{ + if (!std::filesystem::exists(path)) + return false; + + m_assetMgr->clearAllAssetCache(); + + IAssetLoader::SAssetLoadParams params = makeLoadParams(); + auto asset = m_assetMgr->getAsset(path.string(), params); + if (asset.getContents().empty()) + return false; + + core::vector> geometries; + switch (asset.getAssetType()) + { + case IAsset::E_TYPE::ET_GEOMETRY: + for (const auto& item : asset.getContents()) + if (auto polyGeo = IAsset::castDown(item); polyGeo) + geometries.push_back(polyGeo); + break; + default: + return false; + } + return !geometries.empty(); +} + +bool MeshLoadersApp::captureScreenshot(const system::path& path, core::smart_refctd_ptr& outImage) +{ + if (!m_device || !m_surface || !m_assetMgr) + return false; + + m_device->waitIdle(); + + auto* scRes = static_cast(m_surface->getSwapchainResources()); + auto* fb = scRes ? scRes->getFramebuffer(device_base_t::getCurrentAcquire().imageIndex) : nullptr; + if (!fb) + return false; + + auto colorView = fb->getCreationParameters().colorAttachments[0u]; + if (!colorView) + return false; + + auto cpuView = ext::ScreenShot::createScreenShot( + m_device.get(), + getGraphicsQueue(), + nullptr, + colorView.get(), + asset::ACCESS_FLAGS::COLOR_ATTACHMENT_WRITE_BIT, + asset::IImage::LAYOUT::PRESENT_SRC); + if (!cpuView) + return false; + + if (!path.empty()) + std::filesystem::create_directories(path.parent_path()); + + IAssetWriter::SAssetWriteParams params(cpuView.get()); + if (!m_assetMgr->writeAsset(path.string(), params)) + return false; + + outImage = cpuView; + return true; +} + +bool MeshLoadersApp::appendGeometriesFromBundle(const asset::SAssetBundle& bundle, core::vector>& out) const +{ + if (bundle.getContents().empty()) + return false; + + switch (bundle.getAssetType()) + { + case IAsset::E_TYPE::ET_GEOMETRY: + for (const auto& item : bundle.getContents()) + { + if (auto polyGeo = IAsset::castDown(item); polyGeo) + out.push_back(polyGeo); + } + break; + case IAsset::E_TYPE::ET_GEOMETRY_COLLECTION: + for (const auto& item : bundle.getContents()) + { + auto collection = IAsset::castDown(item); + if (!collection) + continue; + auto* refs = collection->getGeometries(); + if (!refs) + continue; + for (const auto& ref : *refs) + { + if (!ref.geometry) + continue; + if (ref.geometry->getPrimitiveType() != IGeometryBase::EPrimitiveType::Polygon) + continue; + auto poly = core::smart_refctd_ptr_static_cast(ref.geometry); + if (poly) + out.push_back(poly); + } + } + break; + default: + return false; + } + + return !out.empty(); +} + +bool MeshLoadersApp::compareImages(const asset::ICPUImageView* a, const asset::ICPUImageView* b, uint64_t& diffCount, uint8_t& maxDiff) +{ + diffCount = 0u; + maxDiff = 0u; + if (!a || !b) + return false; + + const auto* imgA = a->getCreationParameters().image.get(); + const auto* imgB = b->getCreationParameters().image.get(); + if (!imgA || !imgB) + return false; + + const auto paramsA = imgA->getCreationParameters(); + const auto paramsB = imgB->getCreationParameters(); + if (paramsA.format != paramsB.format) + return false; + if (paramsA.extent != paramsB.extent) + return false; + + const auto* bufA = imgA->getBuffer(); + const auto* bufB = imgB->getBuffer(); + if (!bufA || !bufB) + return false; + + const size_t sizeA = bufA->getSize(); + if (sizeA != bufB->getSize()) + return false; + + const auto* dataA = static_cast(bufA->getPointer()); + const auto* dataB = static_cast(bufB->getPointer()); + if (!dataA || !dataB) + return false; + + for (size_t i = 0; i < sizeA; ++i) + { + const uint8_t va = dataA[i]; + const uint8_t vb = dataB[i]; + const uint8_t diff = va > vb ? static_cast(va - vb) : static_cast(vb - va); + if (diff) + { + ++diffCount; + if (diff > maxDiff) + maxDiff = diff; + } + } + + return true; +} + +void MeshLoadersApp::advanceCase() +{ + if (m_runMode == RunMode::Interactive || m_cases.empty()) + return; + if (isRowViewActive()) + return; + + const uint32_t frameLimit = m_runMode == RunMode::CI ? CiFramesBeforeCapture : NonCiFramesPerCase; + ++m_phaseFrameCounter; + if (m_phaseFrameCounter < frameLimit) + return; + + if (m_phase == Phase::RenderOriginal) + { + if (!captureScreenshot(m_loadedScreenshotPath, m_loadedScreenshot)) + failExit("Failed to capture loaded screenshot."); + + if (m_saveGeom) + { + if (!m_currentCpuGeom) + failExit("No geometry to write."); + if (!writeGeometry(m_currentCpuGeom, m_writtenPath.string())) + failExit("Geometry write failed."); + } + + if (m_runMode == RunMode::CI) + { + if (!loadModel(m_writtenPath, false, false)) + failExit("Failed to load written asset %s.", m_writtenPath.string().c_str()); + if (!m_currentCpuGeom) + failExit("Written geometry missing."); + m_phase = Phase::RenderWritten; + m_phaseFrameCounter = 0u; + return; + } + + if (m_saveGeom) + { + if (!validateWrittenAsset(m_writtenPath)) + failExit("Failed to load written asset %s.", m_writtenPath.string().c_str()); + } + + advanceToNextCase(); + return; + } + + if (m_phase == Phase::RenderWritten) + { + if (!captureScreenshot(m_writtenScreenshotPath, m_writtenScreenshot)) + failExit("Failed to capture written screenshot."); + + if (m_hasReferenceGeometryHash) + { + const auto writtenHash = hashGeometry(m_currentCpuGeom.get()); + if (writtenHash != m_referenceGeometryHash) + failExit("Geometry hash mismatch for %s. Current=%s Reference=%s ReferenceFile=%s", m_caseName.c_str(), geometryHashToHex(writtenHash).c_str(), geometryHashToHex(m_referenceGeometryHash).c_str(), m_caseGeometryHashReferencePath.empty() ? "" : m_caseGeometryHashReferencePath.string().c_str()); + } + + uint64_t diffCount = 0u; + uint8_t maxDiff = 0u; + if (!compareImages(m_loadedScreenshot.get(), m_writtenScreenshot.get(), diffCount, maxDiff)) + failExit("Image compare failed for %s.", m_caseName.c_str()); + if (diffCount > MaxImageDiffBytes || maxDiff > MaxImageDiffValue) + failExit("Image diff detected for %s. Bytes: %llu MaxDiff: %u", m_caseName.c_str(), static_cast(diffCount), maxDiff); + if (diffCount != 0u) + m_logger->log("Image diff within tolerance for %s. Bytes: %llu MaxDiff: %u", ILogger::ELL_WARNING, m_caseName.c_str(), static_cast(diffCount), maxDiff); + + advanceToNextCase(); + } +} + + diff --git a/12_MeshLoaders/README.md b/12_MeshLoaders/README.md index 622c56af7..dee05e92b 100644 --- a/12_MeshLoaders/README.md +++ b/12_MeshLoaders/README.md @@ -1,63 +1,92 @@ # 12_MeshLoaders -Loads and writes OBJ, PLY, and STL meshes. Default run reads `meshloaders_inputs.json` from this folder. Relative paths in that file resolve against the JSON file location. +Example for loading and writing `OBJ`, `PLY` and `STL` meshes. -Modes -- Default: row view if `row_view` is true in the JSON -- `--interactive`: single file dialog -- `--ci`: sequential load, write, reload, hash and image compare, then exit +## At a glance +- Default input list: `inputs.json` +- Default mode: `batch` +- Default tuning: `heuristic` +- Loader content hashes: enabled by default +- Output meshes: `saved/` +- Output screenshots: `screenshots/` -Controls (non CI) -- Arrow keys: move camera -- Left mouse drag: rotate -- Home: reset view -- A: add a model to row view -- R: reload test list for row view +## Mode cheat sheet +- `batch` + - Uses test list and runs normal workflow. + - If test list has `row_view: true`, renders all cases in one scene. +- `interactive` + - Opens file dialog and loads one model. +- `ci` + - Runs strict pass/fail validation per case. -Test list -- `cases` can be a list of strings. Each string is a file path relative to the JSON file. +## Common workflows +- Quick visual check: + - run default `batch` +- Inspect one local model: + - run with `--interactive` +- Validate load/write correctness: + - run with `--ci` +- Refresh geometry references: + - run with `--update-references` (usually with `--ci`) -Args +## CLI +- `--ci` + - strict validation run +- `--interactive` + - file-dialog run - `--testlist ` + - custom JSON list - `--savegeometry` + - keep writing output meshes - `--savepath ` + - force output path - `--row-add ` + - add model to row view at startup - `--row-duplicate ` + - duplicate last row-view case +- `--loader-perf-log ` + - redirect loader diagnostics +- `--runtime-tuning ` + - IO runtime tuning mode +- `--loader-content-hashes` + - compatibility switch; already enabled by default +- `--update-references` + - regenerate `references/*.geomhash` + +## Controls (non-CI) +- Arrow keys: move camera +- Left mouse drag: rotate camera +- `Home`: reset view +- `A`: add model to row view +- `R`: reload row view from test list + +## Input list format (`inputs.json`) +```json +{ + "row_view": true, + "cases": [ + "../media/yellowflower.obj", + { "name": "spanner", "path": "../media/ply/Spanner-ply.ply" }, + { "path": "../media/Stanford_Bunny.stl" } + ] +} +``` + +Rules: +- `cases` is required and must be an array +- case item can be string path or object with `path` and optional `name` +- relative paths resolve against JSON file directory + +## What CI validates +- Per-case geometry hash: + - deterministic `BLAKE3` hash compared with `references/*.geomhash` +- Per-case image consistency: + - `*_loaded.png` vs `*_written.png` byte diff + - thresholds come from `MaxImageDiffBytes` and `MaxImageDiffValue` in `MeshLoadersApp.hpp` +- Any mismatch ends with non-zero exit code -Performance (Debug, Win11, Ryzen 5 5600G, RTX 4070, 64 GiB RAM) -- Dataset: - - `yellowflower.obj` (104416 bytes) - - `Spanner-ply.ply` (5700266 bytes) - - `Stanford_Bunny.stl` (5620184 bytes) -- Method: - - 9 sequential runs per format - - compared `master_like_oldalgo` vs `latest_optimized` - - measured `getAsset` and `writeAsset` call times from example logs - -Median summary - -| Asset | Load old ms | Load latest ms | Load speedup x | Write old ms | Write latest ms | Write speedup x | -|---|---:|---:|---:|---:|---:|---:| -| `yellowflower.obj` | 31.657 | 25.988 | 1.22 | 543.659 | 156.585 | 3.47 | -| `Spanner-ply.ply` | 1020.151 | 132.630 | 7.69 | 45.458 | 41.828 | 1.09 | -| `Stanford_Bunny.stl` | 36153.774 | 23.387 | 1545.89 | 17324.853 | 209.200 | 82.81 | - -Why old path was slow -- STL loader used tiny scalar reads in binary path (`4` bytes per float), which amplified IO call overhead. -- STL writer emitted many small writes per triangle (`normal + v0 + v1 + v2 + attr`). -- OBJ/PLY writers performed incremental small writes while building text output. -- IO strategy was hardcoded per loader/writer, without one shared policy for tuning. - -Why current path is better -- One shared `SFileIOPolicy` is available in load/write params for all formats. -- Strategy is explicit (`Auto`, `WholeFile`, `Chunked`) with one resolution path and limits. -- `Auto` can use whole-file for small payloads and chunked IO for larger ones. -- Loader perf logs include requested/effective strategy and timing breakdown. - -Raw benchmark data (full per-run tables) -- `tmp/master_vs_latest_debug.md` -- `tmp/bench_masterlike_vs_latest_debug_2026-02-07_v2/raw_runs.csv` -- `tmp/bench_masterlike_vs_latest_debug_2026-02-07_v2/paired_runs.csv` - -https://github.com/user-attachments/assets/6f779700-e6d4-4e11-95fb-7a7fddc47255 +## Performance logs to trust +- `Asset load call perf` for `getAsset` +- `Asset write call perf` for `writeAsset` +Internal loader stage logs are diagnostics only. diff --git a/12_MeshLoaders/meshloaders_inputs.json b/12_MeshLoaders/inputs.json similarity index 100% rename from 12_MeshLoaders/meshloaders_inputs.json rename to 12_MeshLoaders/inputs.json diff --git a/12_MeshLoaders/main.cpp b/12_MeshLoaders/main.cpp index 5a64ab35a..72f8980a2 100644 --- a/12_MeshLoaders/main.cpp +++ b/12_MeshLoaders/main.cpp @@ -1,2272 +1,7 @@ // Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. // This file is part of the "Nabla Engine". // For conditions of distribution and use, see copyright notice in nabla.h -#include "argparse/argparse.hpp" -#include "common.hpp" -#include "../3rdparty/portable-file-dialogs/portable-file-dialogs.h" -#include "nlohmann/json.hpp" -#include -#include -#include -#include -#include -#include -#include - -#ifdef NBL_BUILD_MITSUBA_LOADER -#include "nbl/ext/MitsubaLoader/CSerializedLoader.h" -#endif - -#ifdef NBL_BUILD_DEBUG_DRAW -#include "nbl/ext/DebugDraw/CDrawAABB.h" -#endif -#include "nbl/ext/ScreenShot/ScreenShot.h" -#include "nbl/system/CFileLogger.h" -#include - -class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourcesApplication -{ - using device_base_t = MonoWindowApplication; - using asset_base_t = BuiltinResourcesApplication; - - enum DrawBoundingBoxMode - { - DBBM_NONE, - DBBM_AABB, - DBBM_OBB, - DBBM_COUNT - }; - - enum class RunMode - { - Interactive, - Batch, - CI - }; - - enum class Phase - { - RenderOriginal, - RenderWritten - }; - - enum class RowViewReloadMode - { - Full, - Incremental - }; - - struct TestCase - { - std::string name; - nbl::system::path path; - }; - - struct CachedGeometryEntry - { - smart_refctd_ptr cpu; - video::asset_cached_t gpu; - hlsl::shapes::AABB<3, double> aabb = hlsl::shapes::AABB<3, double>::create(); - bool hasAabb = false; - }; - - struct RowViewPerfStats - { - double totalMs = 0.0; - double clearMs = 0.0; - double loadMs = 0.0; - double extractMs = 0.0; - double aabbMs = 0.0; - double convertMs = 0.0; - double addGeoMs = 0.0; - double layoutMs = 0.0; - double instanceMs = 0.0; - double cameraMs = 0.0; - size_t cases = 0u; - size_t cpuHits = 0u; - size_t cpuMisses = 0u; - size_t gpuHits = 0u; - size_t gpuMisses = 0u; - size_t convertCount = 0u; - size_t addCount = 0u; - bool incremental = false; - }; - - struct CameraState - { - core::vectorSIMDf position; - core::vectorSIMDf target; - nbl::hlsl::float32_t4x4 projection; - float moveSpeed = 1.0f; - }; - - public: - inline MeshLoadersApp(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD) - : IApplicationFramework(_localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD), - device_base_t({1280,720}, EF_D32_SFLOAT, _localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD) {} - - inline bool onAppInitialized(smart_refctd_ptr&& system) override - { - if (!asset_base_t::onAppInitialized(smart_refctd_ptr(system))) - return false; -#ifdef NBL_BUILD_MITSUBA_LOADER - m_assetMgr->addAssetLoader(make_smart_refctd_ptr()); -#endif - if (!device_base_t::onAppInitialized(smart_refctd_ptr(system))) - return false; - - m_runMode = RunMode::Batch; - m_saveGeomPrefixPath = localOutputCWD / "saved"; - m_screenshotPrefixPath = localOutputCWD / "screenshots"; - m_testListPath = localInputCWD / "meshloaders_inputs.json"; - - argparse::ArgumentParser parser("12_meshloaders"); - parser.add_argument("--savegeometry") - .help("Save the mesh on exit or reload") - .flag(); - - parser.add_argument("--savepath") - .nargs(1) - .help("Specify the file to which the mesh will be saved"); - parser.add_argument("--ci") - .help("Run in CI mode: load test list, write .ply, capture screenshots, compare data, and exit.") - .flag(); - parser.add_argument("--interactive") - .help("Use file dialog to select a single model.") - .flag(); - parser.add_argument("--testlist") - .nargs(1) - .help("JSON file with test cases. Relative paths are resolved against local input CWD."); - parser.add_argument("--row-add") - .nargs(1) - .help("Add a model path to row view on startup without using a dialog."); - parser.add_argument("--row-duplicate") - .nargs(1) - .help("Duplicate the last case N times on startup."); - parser.add_argument("--loader-perf-log") - .nargs(1) - .help("Write loader diagnostics to a file instead of stdout."); - parser.add_argument("--loader-content-hashes") - .help("Force loaders to compute CPU buffer content hashes before returning. Enabled by default.") - .flag(); - parser.add_argument("--runtime-tuning") - .nargs(1) - .help("Runtime tuning mode for loaders: none|heuristic|hybrid. Default: heuristic."); - parser.add_argument("--update-references") - .help("Update or create geometry hash references for CI validation.") - .flag(); - - try - { - parser.parse_args({ argv.data(), argv.data() + argv.size() }); - } - catch (const std::exception& e) - { - return logFail(e.what()); - } - - if (parser["--savegeometry"] == true) - m_saveGeom = true; - if (parser["--interactive"] == true) - m_runMode = RunMode::Interactive; - if (parser["--ci"] == true) - m_runMode = RunMode::CI; - - if (parser.present("--savepath")) - { - auto tmp = path(parser.get("--savepath")); - - if (tmp.empty() || !tmp.has_filename()) - return logFail("Invalid path has been specified in --savepath argument"); - - if (!std::filesystem::exists(tmp.parent_path())) - return logFail("Path specified in --savepath argument doesn't exist"); - - m_specifiedGeomSavePath.emplace(std::move(tmp.generic_string())); - } - - if (parser.present("--testlist")) - { - auto tmp = path(parser.get("--testlist")); - if (tmp.empty()) - return logFail("Invalid path has been specified in --testlist argument"); - if (tmp.is_relative()) - tmp = localInputCWD / tmp; - m_testListPath = tmp; - } - if (parser.present("--row-add")) - { - auto tmp = path(parser.get("--row-add")); - if (tmp.is_relative()) - tmp = localInputCWD / tmp; - m_rowAddPath = tmp; - } - if (parser.present("--row-duplicate")) - { - auto countStr = parser.get("--row-duplicate"); - try - { - m_rowDuplicateCount = static_cast(std::stoul(countStr)); - } - catch (const std::exception&) - { - return logFail("Invalid --row-duplicate value."); - } - } - if (parser.present("--loader-perf-log")) - { - auto tmp = path(parser.get("--loader-perf-log")); - if (tmp.empty()) - return logFail("Invalid --loader-perf-log value."); - if (tmp.is_relative()) - tmp = localOutputCWD / tmp; - m_loaderPerfLogPath = tmp; - } - if (parser["--update-references"] == true) - m_updateGeometryHashReferences = true; - if (parser["--loader-content-hashes"] == true) - m_forceLoaderContentHashes = true; - if (parser.present("--runtime-tuning")) - { - auto mode = parser.get("--runtime-tuning"); - std::transform(mode.begin(), mode.end(), mode.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); - if (mode == "none") - m_runtimeTuningMode = asset::SFileIOPolicy::SRuntimeTuning::Mode::None; - else if (mode == "heuristic") - m_runtimeTuningMode = asset::SFileIOPolicy::SRuntimeTuning::Mode::Heuristic; - else if (mode == "hybrid") - m_runtimeTuningMode = asset::SFileIOPolicy::SRuntimeTuning::Mode::Hybrid; - else - return logFail("Invalid --runtime-tuning value. Expected: none|heuristic|hybrid."); - } - - const path inputReferencesDir = localInputCWD / "references"; - const path outputReferencesDir = localOutputCWD / "references"; - std::error_code referenceDirEc; - const bool hasInputReferencesDir = std::filesystem::is_directory(inputReferencesDir, referenceDirEc) && !referenceDirEc; - referenceDirEc.clear(); - const bool hasOutputReferencesDir = std::filesystem::is_directory(outputReferencesDir, referenceDirEc) && !referenceDirEc; - m_geometryHashReferenceDir = hasOutputReferencesDir || !hasInputReferencesDir ? outputReferencesDir : inputReferencesDir; - if (hasOutputReferencesDir && !hasInputReferencesDir) - m_logger->log("Geometry hash references resolved to output directory: %s", system::ILogger::ELL_INFO, m_geometryHashReferenceDir.string().c_str()); - if (m_runMode == RunMode::CI || m_updateGeometryHashReferences) - { - std::error_code ec; - std::filesystem::create_directories(m_geometryHashReferenceDir, ec); - if (ec) - return logFail("Failed to create geometry hash reference directory: %s", m_geometryHashReferenceDir.string().c_str()); - } - - if (m_saveGeom) - std::filesystem::create_directories(m_saveGeomPrefixPath); - std::filesystem::create_directories(m_screenshotPrefixPath); - m_assetLoadLogger = m_logger; - if (m_loaderPerfLogPath) - { - if (!initLoaderPerfLogger(*m_loaderPerfLogPath)) - return false; - m_logger->log("Loader diagnostics will be written to %s", ILogger::ELL_INFO, m_loaderPerfLogPath->string().c_str()); - } - - m_semaphore = m_device->createSemaphore(m_realFrameIx); - if (!m_semaphore) - return logFail("Failed to Create a Semaphore!"); - - auto pool = m_device->createCommandPool(getGraphicsQueue()->getFamilyIndex(), IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT); - for (auto i = 0u; i < MaxFramesInFlight; i++) - { - if (!pool) - return logFail("Couldn't create Command Pool!"); - if (!pool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, { m_cmdBufs.data() + i,1 })) - return logFail("Couldn't create Command Buffer!"); - } - - auto scRes = static_cast(m_surface->getSwapchainResources()); - m_renderer = CSimpleDebugRenderer::create(m_assetMgr.get(), scRes->getRenderpass(), 0, {}); - if (!m_renderer) - return logFail("Failed to create renderer!"); - -#ifdef NBL_BUILD_DEBUG_DRAW - { - auto* renderpass = scRes->getRenderpass(); - ext::debug_draw::DrawAABB::SCreationParameters params = {}; - params.assetManager = m_assetMgr; - params.transfer = getTransferUpQueue(); - params.drawMode = ext::debug_draw::DrawAABB::ADM_DRAW_BATCH; - params.batchPipelineLayout = ext::debug_draw::DrawAABB::createDefaultPipelineLayout(m_device.get()); - params.renderpass = smart_refctd_ptr(renderpass); - params.utilities = m_utils; - m_drawAABB = ext::debug_draw::DrawAABB::create(std::move(params)); - } -#endif - - if (!initTestCases()) - return false; - - if (isRowViewActive()) - { - m_nonInteractiveTest = false; - if (!loadRowView(RowViewReloadMode::Full)) - return false; - if (m_rowAddPath) - if (!addRowViewCaseFromPath(*m_rowAddPath)) - return false; - if (m_rowDuplicateCount > 0u && !m_cases.empty()) - { - const auto lastPath = m_cases.back().path; - for (uint32_t i = 0u; i < m_rowDuplicateCount; ++i) - if (!addRowViewCaseFromPath(lastPath)) - return false; - } - } - else - { - if (m_runMode != RunMode::Interactive) - m_nonInteractiveTest = true; - if (!startCase(0u)) - return false; - } - - camera.mapKeysToArrows(); - - onAppInitializedFinish(); - return true; - } - - inline IQueue::SSubmitInfo::SSemaphoreInfo renderFrame(const std::chrono::microseconds nextPresentationTimestamp) override - { - m_inputSystem->getDefaultMouse(&mouse); - m_inputSystem->getDefaultKeyboard(&keyboard); - - // - const auto resourceIx = m_realFrameIx % MaxFramesInFlight; - - auto* const cb = m_cmdBufs.data()[resourceIx].get(); - cb->reset(IGPUCommandBuffer::RESET_FLAGS::RELEASE_RESOURCES_BIT); - cb->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - // clear to black for both things - { - // begin renderpass - { - auto scRes = static_cast(m_surface->getSwapchainResources()); - auto* framebuffer = scRes->getFramebuffer(device_base_t::getCurrentAcquire().imageIndex); - const IGPUCommandBuffer::SClearColorValue clearValue = { .float32 = {1.f,0.f,1.f,1.f} }; - const IGPUCommandBuffer::SClearDepthStencilValue depthValue = { .depth = 0.f }; - const VkRect2D currentRenderArea = - { - .offset = {0,0}, - .extent = {framebuffer->getCreationParameters().width,framebuffer->getCreationParameters().height} - }; - const IGPUCommandBuffer::SRenderpassBeginInfo info = - { - .framebuffer = framebuffer, - .colorClearValues = &clearValue, - .depthStencilClearValues = &depthValue, - .renderArea = currentRenderArea - }; - cb->beginRenderPass(info, IGPUCommandBuffer::SUBPASS_CONTENTS::INLINE); - - const SViewport viewport = { - .x = static_cast(currentRenderArea.offset.x), - .y = static_cast(currentRenderArea.offset.y), - .width = static_cast(currentRenderArea.extent.width), - .height = static_cast(currentRenderArea.extent.height) - }; - cb->setViewport(0u,1u,&viewport); - - cb->setScissor(0u,1u,¤tRenderArea); - } - // late latch input - if (!m_nonInteractiveTest) - { - bool reloadInteractiveRequested = false; - bool reloadListRequested = false; - bool addRowViewRequested = false; - camera.beginInputProcessing(nextPresentationTimestamp); - mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void { camera.mouseProcess(events); }, m_logger.get()); - keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t& events) -> void - { - for (const auto& event : events) - { - if (event.action != SKeyboardEvent::ECA_RELEASED) - continue; - if (event.keyCode == E_KEY_CODE::EKC_R) - { - if (isRowViewActive()) - reloadListRequested = true; - else - reloadInteractiveRequested = true; - } - else if (event.keyCode == E_KEY_CODE::EKC_A) - { - if (isRowViewActive()) - addRowViewRequested = true; - } - } - camera.keyboardProcess(events); - }, - m_logger.get() - ); - camera.endInputProcessing(nextPresentationTimestamp); - if (addRowViewRequested) - addRowViewCase(); - if (reloadListRequested) - { - if (!reloadFromTestList()) - failExit("Failed to reload test list."); - } - if (reloadInteractiveRequested) - reloadInteractive(); - } - // draw scene - const auto& viewMatrix = camera.getViewMatrix(); - const auto& viewProjMatrix = camera.getConcatenatedMatrix(); - { - m_renderer->render(cb,CSimpleDebugRenderer::SViewParams(viewMatrix,viewProjMatrix)); - } -#ifdef NBL_BUILD_DEBUG_DRAW - { - const ISemaphore::SWaitInfo drawFinished = { .semaphore = m_semaphore.get(),.value = m_realFrameIx + 1u }; - ext::debug_draw::DrawAABB::DrawParameters drawParams; - drawParams.commandBuffer = cb; - drawParams.cameraMat = viewProjMatrix; - m_drawAABB->render(drawParams, drawFinished, m_aabbInstances); - } -#endif - cb->endRenderPass(); - } - cb->end(); - - IQueue::SSubmitInfo::SSemaphoreInfo retval = - { - .semaphore = m_semaphore.get(), - .value = ++m_realFrameIx, - .stageMask = PIPELINE_STAGE_FLAGS::ALL_GRAPHICS_BITS - }; - const IQueue::SSubmitInfo::SCommandBufferInfo commandBuffers[] = - { - {.cmdbuf = cb } - }; - const IQueue::SSubmitInfo::SSemaphoreInfo acquired[] = { - { - .semaphore = device_base_t::getCurrentAcquire().semaphore, - .value = device_base_t::getCurrentAcquire().acquireCount, - .stageMask = PIPELINE_STAGE_FLAGS::NONE - } - }; - const IQueue::SSubmitInfo infos[] = - { - { - .waitSemaphores = acquired, - .commandBuffers = commandBuffers, - .signalSemaphores = {&retval,1} - } - }; - - if (getGraphicsQueue()->submit(infos) != IQueue::RESULT::SUCCESS) - { - retval.semaphore = nullptr; // so that we don't wait on semaphore that will never signal - m_realFrameIx--; - } - - std::string caption = "[Nabla Engine] Mesh Loaders"; - { - caption += ", displaying ["; - caption += m_modelPath; - caption += "]"; - m_window->setCaption(caption); - } - if (isRowViewActive() && !m_rowViewScreenshotCaptured && m_realFrameIx >= RowViewFramesBeforeCapture) - { - if (!captureScreenshot(m_rowViewScreenshotPath, m_loadedScreenshot)) - failExit("Failed to capture row view screenshot."); - m_rowViewScreenshotCaptured = true; - } - advanceCase(); - return retval; - } - - inline bool onAppTerminated() override - { - return device_base_t::onAppTerminated(); - } - - inline bool keepRunning() override - { - if (m_shouldQuit) - return false; - return device_base_t::keepRunning(); - } - -protected: - const video::IGPURenderpass::SCreationParams::SSubpassDependency* getDefaultSubpassDependencies() const override - { - // Subsequent submits don't wait for each other, hence its important to have External Dependencies which prevent users of the depth attachment overlapping. - const static IGPURenderpass::SCreationParams::SSubpassDependency dependencies[] = { - // wipe-transition of Color to ATTACHMENT_OPTIMAL and depth - { - .srcSubpass = IGPURenderpass::SCreationParams::SSubpassDependency::External, - .dstSubpass = 0, - .memoryBarrier = { - // last place where the depth can get modified in previous frame, `COLOR_ATTACHMENT_OUTPUT_BIT` is implicitly later - .srcStageMask = PIPELINE_STAGE_FLAGS::LATE_FRAGMENT_TESTS_BIT, - // don't want any writes to be available, we'll clear - .srcAccessMask = ACCESS_FLAGS::NONE, - // destination needs to wait as early as possible - // TODO: `COLOR_ATTACHMENT_OUTPUT_BIT` shouldn't be needed, because its a logically later stage, see TODO in `ECommonEnums.h` - .dstStageMask = PIPELINE_STAGE_FLAGS::EARLY_FRAGMENT_TESTS_BIT | PIPELINE_STAGE_FLAGS::COLOR_ATTACHMENT_OUTPUT_BIT, - // because depth and color get cleared first no read mask - .dstAccessMask = ACCESS_FLAGS::DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | ACCESS_FLAGS::COLOR_ATTACHMENT_WRITE_BIT - } - // leave view offsets and flags default - }, - // color from ATTACHMENT_OPTIMAL to PRESENT_SRC - { - .srcSubpass = 0, - .dstSubpass = IGPURenderpass::SCreationParams::SSubpassDependency::External, - .memoryBarrier = { - // last place where the color can get modified, depth is implicitly earlier - .srcStageMask = PIPELINE_STAGE_FLAGS::COLOR_ATTACHMENT_OUTPUT_BIT, - // only write ops, reads can't be made available - .srcAccessMask = ACCESS_FLAGS::COLOR_ATTACHMENT_WRITE_BIT - // spec says nothing is needed when presentation is the destination - } - // leave view offsets and flags default - }, - IGPURenderpass::SCreationParams::DependenciesEnd - }; - return dependencies; - } - -private: - // TODO: standardise this across examples, and take from `argv` - bool m_nonInteractiveTest = false; - bool m_rowViewEnabled = true; - bool m_rowViewScreenshotCaptured = false; - - template - [[noreturn]] void failExit(const char* msg, Args... args) - { - if (m_logger) - m_logger->log(msg, ILogger::ELL_ERROR, args...); - std::exit(-1); - } - - bool initTestCases() - { - m_cases.clear(); - m_caseNameCounts.clear(); - if (m_runMode == RunMode::Interactive) - { - system::path picked; - if (!pickModelPath(picked)) - return logFail("No file selected."); - m_cases.push_back({ makeUniqueCaseName(picked), picked }); - return true; - } - return loadTestList(m_testListPath); - } - - bool pickModelPath(system::path& outPath) - { - pfd::open_file file("Choose a supported Model File", sharedInputCWD.string(), - { - "All Supported Formats", "*.ply *.stl *.serialized *.obj", - "TODO (.ply)", "*.ply", - "TODO (.stl)", "*.stl", - "Mitsuba 0.6 Serialized (.serialized)", "*.serialized", - "Wavefront Object (.obj)", "*.obj" - }, - false - ); - if (file.result().empty()) - return false; - outPath = file.result()[0]; - return true; - } - - bool loadTestList(const system::path& jsonPath) - { - if (!std::filesystem::exists(jsonPath)) - return logFail("Missing test list: %s", jsonPath.string().c_str()); - - std::ifstream stream(jsonPath); - if (!stream.is_open()) - return logFail("Failed to open test list: %s", jsonPath.string().c_str()); - - nlohmann::json doc; - try - { - stream >> doc; - } - catch (const std::exception& e) - { - return logFail("Invalid JSON in test list: %s", e.what()); - } - - if (!doc.contains("cases") || !doc["cases"].is_array()) - return logFail("Test list JSON missing \"cases\" array."); - - m_caseNameCounts.clear(); - - if (doc.contains("row_view")) - { - if (!doc["row_view"].is_boolean()) - return logFail("\"row_view\" must be a boolean."); - m_rowViewEnabled = doc["row_view"].get(); - } - - const auto baseDir = jsonPath.parent_path(); - for (const auto& entry : doc["cases"]) - { - std::string pathString; - - if (entry.is_string()) - { - pathString = entry.get(); - } - else if (entry.is_object()) - { - if (!entry.contains("path") || !entry["path"].is_string()) - return logFail("Test list entry missing \"path\"."); - pathString = entry["path"].get(); - } - else - return logFail("Invalid test list entry."); - - system::path path = pathString; - if (path.is_relative()) - path = baseDir / path; - if (!std::filesystem::exists(path)) - return logFail("Missing test input: %s", path.string().c_str()); - - m_cases.push_back({ makeUniqueCaseName(path), path }); - } - - if (m_cases.empty()) - return logFail("No test cases in test list."); - - return true; - } - - bool isRowViewActive() const - { - return m_rowViewEnabled && m_runMode != RunMode::CI && m_runMode != RunMode::Interactive; - } - - static inline std::string normalizeExtension(const system::path& path) - { - auto ext = path.extension().string(); - std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); - return ext; - } - - bool isWriteExtensionSupported(const std::string& ext) const - { - if (ext == ".ply" || ext == ".stl") - return true; -#ifdef _NBL_COMPILE_WITH_OBJ_WRITER_ - if (ext == ".obj") - return true; -#endif - return false; - } - - system::path resolveSavePath(const system::path& modelPath) const - { - if (m_specifiedGeomSavePath) - return path(*m_specifiedGeomSavePath); - const auto stem = modelPath.stem().string(); - auto ext = normalizeExtension(modelPath); - if (ext.empty()) - ext = ".ply"; - if (!isWriteExtensionSupported(ext)) - { - if (m_logger) - m_logger->log("No writer for %s, writing .ply instead.", ILogger::ELL_WARNING, ext.c_str()); - ext = ".ply"; - } - return m_saveGeomPrefixPath / (stem + "_written" + ext); - } - - static inline std::string sanitizeCaseNameForFilename(std::string name) - { - for (auto& ch : name) - { - const unsigned char uch = static_cast(ch); - if (!(std::isalnum(uch) || ch == '_' || ch == '-' || ch == '.')) - ch = '_'; - } - if (name.empty()) - name = "unnamed_case"; - return name; - } - - system::path getGeometryHashReferencePath(const std::string& caseName) const - { - return m_geometryHashReferenceDir / (sanitizeCaseNameForFilename(caseName) + ".geomhash"); - } - - static inline std::string geometryHashToHex(const core::blake3_hash_t& hash) - { - static constexpr char HexDigits[] = "0123456789abcdef"; - std::string out; - out.resize(sizeof(hash.data) * 2ull); - for (size_t i = 0ull; i < sizeof(hash.data); ++i) - { - const uint8_t v = hash.data[i]; - out[2ull * i + 0ull] = HexDigits[(v >> 4) & 0xfu]; - out[2ull * i + 1ull] = HexDigits[v & 0xfu]; - } - return out; - } - - static inline bool tryParseNibble(const char c, uint8_t& out) - { - if (c >= '0' && c <= '9') - { - out = static_cast(c - '0'); - return true; - } - if (c >= 'a' && c <= 'f') - { - out = static_cast(10 + c - 'a'); - return true; - } - if (c >= 'A' && c <= 'F') - { - out = static_cast(10 + c - 'A'); - return true; - } - return false; - } - - static inline bool tryParseGeometryHashHex(std::string hex, core::blake3_hash_t& outHash) - { - hex.erase(std::remove_if(hex.begin(), hex.end(), [](unsigned char c) { return std::isspace(c) != 0; }), hex.end()); - if (hex.size() != sizeof(outHash.data) * 2ull) - return false; - - for (size_t i = 0ull; i < sizeof(outHash.data); ++i) - { - uint8_t hi = 0u; - uint8_t lo = 0u; - if (!tryParseNibble(hex[2ull * i + 0ull], hi) || !tryParseNibble(hex[2ull * i + 1ull], lo)) - return false; - outHash.data[i] = static_cast((hi << 4) | lo); - } - return true; - } - - bool readGeometryHashReference(const system::path& refPath, core::blake3_hash_t& outHash) const - { - std::ifstream in(refPath); - if (!in.is_open()) - return false; - std::string line; - std::getline(in, line); - return tryParseGeometryHashHex(std::move(line), outHash); - } - - bool writeGeometryHashReference(const system::path& refPath, const core::blake3_hash_t& hash) const - { - std::error_code ec; - std::filesystem::create_directories(refPath.parent_path(), ec); - if (ec) - return false; - std::ofstream out(refPath, std::ios::binary | std::ios::trunc); - if (!out.is_open()) - return false; - out << geometryHashToHex(hash) << '\n'; - return out.good(); - } - - bool startCase(const size_t index) - { - if (index >= m_cases.size()) - return false; - - m_caseIndex = index; - m_phase = Phase::RenderOriginal; - m_phaseFrameCounter = 0u; - m_loadedScreenshot = nullptr; - m_writtenScreenshot = nullptr; - m_referenceCamera.reset(); - m_referenceCpuGeom = nullptr; - m_hasReferenceGeometry = false; - m_hasReferenceGeometryHash = false; - m_caseGeometryHashReferencePath.clear(); - - const auto& testCase = m_cases[m_caseIndex]; - m_caseName = testCase.name.empty() ? testCase.path.stem().string() : testCase.name; - m_writtenPath = resolveSavePath(testCase.path); - m_loadedScreenshotPath = m_screenshotPrefixPath / ("meshloaders_" + m_caseName + "_loaded.png"); - m_writtenScreenshotPath = m_screenshotPrefixPath / ("meshloaders_" + m_caseName + "_written.png"); - - if (!loadModel(testCase.path, true, true)) - return false; - - if (m_currentCpuGeom) - { - m_referenceCpuGeom = m_currentCpuGeom; - m_hasReferenceGeometry = true; - const auto loadedGeometryHash = hashGeometry(m_referenceCpuGeom.get()); - m_referenceGeometryHash = loadedGeometryHash; - m_hasReferenceGeometryHash = true; - m_caseGeometryHashReferencePath = getGeometryHashReferencePath(m_caseName); - - if (m_updateGeometryHashReferences) - { - const bool referenceExisted = std::filesystem::exists(m_caseGeometryHashReferencePath); - if (!writeGeometryHashReference(m_caseGeometryHashReferencePath, loadedGeometryHash)) - return logFail("Failed to write geometry hash reference: %s", m_caseGeometryHashReferencePath.string().c_str()); - if (!referenceExisted) - m_logger->log("Geometry hash reference did not exist for %s. Created new reference at %s", ILogger::ELL_WARNING, m_caseName.c_str(), m_caseGeometryHashReferencePath.string().c_str()); - else - m_logger->log("Geometry hash reference updated for %s at %s", ILogger::ELL_INFO, m_caseName.c_str(), m_caseGeometryHashReferencePath.string().c_str()); - } - else if (m_runMode == RunMode::CI) - { - if (!std::filesystem::exists(m_caseGeometryHashReferencePath)) - return logFail("Missing geometry hash reference for %s at %s. Run once with --update-references.", m_caseName.c_str(), m_caseGeometryHashReferencePath.string().c_str()); - - core::blake3_hash_t onDiskHash = {}; - if (!readGeometryHashReference(m_caseGeometryHashReferencePath, onDiskHash)) - return logFail("Invalid geometry hash reference for %s at %s", m_caseName.c_str(), m_caseGeometryHashReferencePath.string().c_str()); - - m_referenceGeometryHash = onDiskHash; - m_hasReferenceGeometryHash = true; - if (loadedGeometryHash != onDiskHash) - { - m_logger->log("Loaded geometry hash mismatch for %s. Current=%s Reference=%s", ILogger::ELL_ERROR, m_caseName.c_str(), geometryHashToHex(loadedGeometryHash).c_str(), geometryHashToHex(onDiskHash).c_str()); - return logFail("Loaded asset differs from stored geometry hash reference for %s.", m_caseName.c_str()); - } - } - } - - return true; - } - - bool advanceToNextCase() - { - const auto nextIndex = m_caseIndex + 1u; - if (nextIndex >= m_cases.size()) - { - m_shouldQuit = true; - return false; - } - if (!startCase(nextIndex)) - { - m_shouldQuit = true; - return false; - } - return true; - } - - void reloadInteractive() - { - system::path picked; - if (!pickModelPath(picked)) - failExit("No file selected."); - if (!loadModel(picked, true, true)) - failExit("Failed to load asset %s.", picked.string().c_str()); - if (m_currentCpuGeom && m_saveGeom) - { - const auto savePath = resolveSavePath(picked); - if (!writeGeometry(m_currentCpuGeom, savePath.string())) - failExit("Geometry write failed."); - } - } - - bool addRowViewCase() - { - system::path picked; - if (!pickModelPath(picked)) - return false; - return addRowViewCaseFromPath(picked); - } - - bool addRowViewCaseFromPath(const system::path& picked) - { - if (picked.empty()) - return false; - m_cases.push_back({ makeUniqueCaseName(picked), picked }); - m_shouldQuit = false; - return loadRowView(RowViewReloadMode::Incremental); - } - - bool reloadFromTestList() - { - m_cases.clear(); - if (!loadTestList(m_testListPath)) - return false; - m_shouldQuit = false; - m_rowViewScreenshotCaptured = false; - if (isRowViewActive()) - { - m_nonInteractiveTest = false; - return loadRowView(RowViewReloadMode::Full); - } - m_nonInteractiveTest = (m_runMode != RunMode::Interactive); - return startCase(0u); - } - - bool loadModel(const system::path& modelPath, const bool updateCamera, const bool storeCamera) - { - if (modelPath.empty()) - failExit("Empty model path."); - if (!std::filesystem::exists(modelPath)) - failExit("Missing input: %s", modelPath.string().c_str()); - using clock_t = std::chrono::high_resolution_clock; - const auto loadOuterStart = clock_t::now(); - - m_modelPath = modelPath.string(); - - // free up - m_renderer->m_instances.clear(); - m_renderer->clearGeometries({ .semaphore = m_semaphore.get(),.value = m_realFrameIx }); - m_assetMgr->clearAllAssetCache(); - - //! load the geometry - IAssetLoader::SAssetLoadParams params = makeLoadParams(); - AssetLoadCallResult loadResult = {}; - if (!loadAssetCallFromPath(modelPath, params, loadResult)) - failExit("Failed to open input file %s.", modelPath.string().c_str()); - if (loadResult.fileFlags != 0u) - { - m_logger->log( - "Input file mapping probe: path=%s flags=0x%X mapped=%d", - ILogger::ELL_PERFORMANCE, - m_modelPath.c_str(), - loadResult.fileFlags, - loadResult.mapped ? 1 : 0); - } - const auto openMs = loadResult.openMs; - const auto loadMs = loadResult.getAssetMs; - auto asset = std::move(loadResult.bundle); - m_logger->log( - "Asset load call perf: path=%s time=%.3f ms size=%llu", - ILogger::ELL_INFO, - m_modelPath.c_str(), - loadMs, - static_cast(loadResult.inputSize)); - if (asset.getContents().empty()) - failExit("Failed to load asset %s.", m_modelPath.c_str()); - - // - core::vector> geometries; - const auto extractStart = clock_t::now(); - if (!appendGeometriesFromBundle(asset, geometries)) - failExit("Asset loaded but not a supported type for %s.", m_modelPath.c_str()); - const auto extractMs = toMs(clock_t::now() - extractStart); - if (geometries.empty()) - failExit("No geometry found in asset %s.", m_modelPath.c_str()); - const auto outerMs = toMs(clock_t::now() - loadOuterStart); - const auto nonLoaderMs = std::max(0.0, outerMs - loadMs); - m_logger->log( - "Asset load outer perf: path=%s open=%.3f ms getAsset=%.3f ms extract=%.3f ms total=%.3f ms non_loader=%.3f ms", - ILogger::ELL_INFO, - m_modelPath.c_str(), - openMs, - loadMs, - extractMs, - outerMs, - nonLoaderMs); - - m_currentCpuGeom = geometries[0]; - - using aabb_t = hlsl::shapes::AABB<3, double>; - auto printAABB = [&](const aabb_t& aabb, const char* extraMsg = "")->void - { - m_logger->log("%s AABB is (%f,%f,%f) -> (%f,%f,%f)", ILogger::ELL_INFO, extraMsg, aabb.minVx.x, aabb.minVx.y, aabb.minVx.z, aabb.maxVx.x, aabb.maxVx.y, aabb.maxVx.z); - }; - auto bound = aabb_t::create(); - // convert the geometries - { - smart_refctd_ptr converter = CAssetConverter::create({ .device = m_device.get() }); - - const auto transferFamily = getTransferUpQueue()->getFamilyIndex(); - - struct SInputs : CAssetConverter::SInputs - { - virtual inline std::span getSharedOwnershipQueueFamilies(const size_t groupCopyID, const asset::ICPUBuffer* buffer, const CAssetConverter::patch_t& patch) const - { - return sharedBufferOwnership; - } - - core::vector sharedBufferOwnership; - } inputs = {}; - core::vector> patches(geometries.size(), CSimpleDebugRenderer::DefaultPolygonGeometryPatch); - { - inputs.logger = m_logger.get(); - std::get>(inputs.assets) = { &geometries.front().get(),geometries.size() }; - std::get>(inputs.patches) = patches; - // set up shared ownership so we don't have to - core::unordered_set families; - families.insert(transferFamily); - families.insert(getGraphicsQueue()->getFamilyIndex()); - if (families.size() > 1) - for (const auto fam : families) - inputs.sharedBufferOwnership.push_back(fam); - } - - // reserve - auto reservation = converter->reserve(inputs); - if (!reservation) - { - failExit("Failed to reserve GPU objects for CPU->GPU conversion."); - } - - // convert - { - auto semaphore = m_device->createSemaphore(0u); - - constexpr auto MultiBuffering = 2; - std::array, MultiBuffering> commandBuffers = {}; - { - auto pool = m_device->createCommandPool(transferFamily, IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT | IGPUCommandPool::CREATE_FLAGS::TRANSIENT_BIT); - pool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, commandBuffers, smart_refctd_ptr(m_logger)); - } - commandBuffers.front()->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - - std::array commandBufferSubmits; - for (auto i = 0; i < MultiBuffering; i++) - commandBufferSubmits[i].cmdbuf = commandBuffers[i].get(); - - SIntendedSubmitInfo transfer = {}; - transfer.queue = getTransferUpQueue(); - transfer.scratchCommandBuffers = commandBufferSubmits; - transfer.scratchSemaphore = { - .semaphore = semaphore.get(), - .value = 0u, - .stageMask = PIPELINE_STAGE_FLAGS::ALL_TRANSFER_BITS - }; - - CAssetConverter::SConvertParams cpar = {}; - cpar.utilities = m_utils.get(); - cpar.transfer = &transfer; - - // basically it records all data uploads and submits them right away - auto future = reservation.convert(cpar); - if (future.copy()!=IQueue::RESULT::SUCCESS) - { - failExit("Failed to await submission feature."); - } - } - - auto tmp = hlsl::float32_t4x3( - hlsl::float32_t3(1,0,0), - hlsl::float32_t3(0,1,0), - hlsl::float32_t3(0,0,1), - hlsl::float32_t3(0,0,0) - ); - core::vector worldTforms; - const auto& converted = reservation.getGPUObjects(); - m_aabbInstances.resize(converted.size()); - if (m_drawBBMode == DBBM_OBB) - m_obbInstances.resize(converted.size()); - for (uint32_t i = 0; i < converted.size(); i++) - { - const auto& geom = converted[i]; - const auto& cpuGeom = geometries[i].get(); - const auto promoted = getGeometryAABB(cpuGeom); - printAABB(promoted,"Geometry"); - const auto promotedWorld = hlsl::float64_t3x4(worldTforms.emplace_back(hlsl::transpose(tmp))); - const auto translation = hlsl::float64_t3( - static_cast(tmp[3].x), - static_cast(tmp[3].y), - static_cast(tmp[3].z)); - const auto transformed = translateAABB(promoted, translation); - printAABB(transformed,"Transformed"); - bound = hlsl::shapes::util::union_(transformed,bound); - -#ifdef NBL_BUILD_DEBUG_DRAW - - auto& aabbInst = m_aabbInstances[i]; - const auto tmpAabb = shapes::AABB<3,float>(promoted.minVx, promoted.maxVx); - - hlsl::float32_t3x4 aabbTransform = ext::debug_draw::DrawAABB::getTransformFromAABB(tmpAabb); - const auto tmpWorld = hlsl::float32_t3x4(promotedWorld); - const auto world4x4 = float32_t4x4{ - tmpWorld[0], - tmpWorld[1], - tmpWorld[2], - float32_t4(0, 0, 0, 1) - }; - - aabbInst.color = { 1,1,1,1 }; - aabbInst.transform = math::linalg::promoted_mul(world4x4, aabbTransform); - - if (m_drawBBMode == DBBM_OBB) - { - auto& obbInst = m_obbInstances[i]; - const auto obb = CPolygonGeometryManipulator::calculateOBB( - cpuGeom->getPositionView().getElementCount(), - [geo = cpuGeom, &world4x4](size_t vertex_i) { - hlsl::float32_t3 pt; - geo->getPositionView().decodeElement(vertex_i, pt); - return pt; - }); - obbInst.color = { 0, 0, 1, 1 }; - obbInst.transform = math::linalg::promoted_mul(world4x4, obb.transform); - } -#endif - } - - printAABB(bound,"Total"); - if (!m_renderer->addGeometries({ &converted.front().get(),converted.size() })) - failExit("Failed to add geometries to renderer."); - if (m_logger) - { - const auto& gpuGeos = m_renderer->getGeometries(); - for (size_t geoIx = 0u; geoIx < gpuGeos.size(); ++geoIx) - { - const auto& gpuGeo = gpuGeos[geoIx]; - m_logger->log( - "Renderer geo state: idx=%llu elem=%u posView=%u normalView=%u indexType=%u", - ILogger::ELL_DEBUG, - static_cast(geoIx), - gpuGeo.elementCount, - static_cast(gpuGeo.positionView), - static_cast(gpuGeo.normalView), - static_cast(gpuGeo.indexType)); - } - } - - auto worlTformsIt = worldTforms.begin(); - for (const auto& geo : m_renderer->getGeometries()) - m_renderer->m_instances.push_back({ - .world = *(worlTformsIt++), - .packedGeo = &geo - }); - } - - if (updateCamera) - { - setupCameraFromAABB(bound); - if (storeCamera) - storeCameraState(); - } - else if (m_referenceCamera) - applyCameraState(*m_referenceCamera); - else - setupCameraFromAABB(bound); - - return true; - } - - bool loadRowView(const RowViewReloadMode mode) - { - if (m_cases.empty()) - failExit("No test cases loaded for row view."); - - using clock_t = std::chrono::high_resolution_clock; - RowViewPerfStats stats = {}; - stats.incremental = (mode == RowViewReloadMode::Incremental); - stats.cases = m_cases.size(); - const auto totalStart = clock_t::now(); - - const auto clearStart = clock_t::now(); - if (mode == RowViewReloadMode::Full) - { - m_renderer->m_instances.clear(); - m_renderer->clearGeometries({ .semaphore = m_semaphore.get(),.value = m_realFrameIx }); - } - stats.clearMs = toMs(clock_t::now() - clearStart); - - core::vector> geometries; - core::vector> aabbs; - geometries.reserve(m_cases.size()); - aabbs.reserve(m_cases.size()); - - core::vector> cpuToConvert; - core::vector convertEntries; - - m_rowViewCache.reserve(m_cases.size()); - - IAssetLoader::SAssetLoadParams params = makeLoadParams(); - - for (const auto& testCase : m_cases) - { - const auto& path = testCase.path; - if (!std::filesystem::exists(path)) - failExit("Missing input: %s", path.string().c_str()); - - const auto cacheKey = makeCacheKey(path); - auto& entry = m_rowViewCache[cacheKey]; - double assetLoadMs = 0.0; - bool cached = true; - if (!entry.cpu) - { - stats.cpuMisses++; - cached = false; - AssetLoadCallResult loadResult = {}; - if (!loadAssetCallFromPath(path, params, loadResult)) - failExit("Failed to open input file %s.", path.string().c_str()); - auto asset = std::move(loadResult.bundle); - assetLoadMs = loadResult.getAssetMs; - stats.loadMs += assetLoadMs; - if (asset.getContents().empty()) - failExit("Failed to load asset %s.", path.string().c_str()); - - const auto extractStart = clock_t::now(); - core::vector> found; - if (appendGeometriesFromBundle(asset, found)) - { - if (!found.empty()) - entry.cpu = found.front(); - } - stats.extractMs += toMs(clock_t::now() - extractStart); - if (!entry.cpu) - failExit("No geometry found in asset %s.", path.string().c_str()); - - const auto aabbStart = clock_t::now(); - entry.aabb = getGeometryAABB(entry.cpu.get()); - entry.hasAabb = isValidAABB(entry.aabb); - stats.aabbMs += toMs(clock_t::now() - aabbStart); - } - else - { - stats.cpuHits++; - if (!entry.hasAabb) - { - const auto aabbStart = clock_t::now(); - entry.aabb = getGeometryAABB(entry.cpu.get()); - entry.hasAabb = isValidAABB(entry.aabb); - stats.aabbMs += toMs(clock_t::now() - aabbStart); - } - } - logRowViewAssetLoad(path, assetLoadMs, cached); - - if (!entry.gpu) - { - stats.gpuMisses++; - cpuToConvert.push_back(entry.cpu); - convertEntries.push_back(&entry); - } - else - { - stats.gpuHits++; - } - - geometries.push_back(entry.cpu); - aabbs.push_back(entry.aabb); - } - - if (geometries.empty()) - failExit("No geometry found for row view."); - logRowViewLoadTotal(stats.loadMs, stats.cpuHits, stats.cpuMisses); - - if (!cpuToConvert.empty()) - { - stats.convertCount = cpuToConvert.size(); - const auto convertStart = clock_t::now(); - - smart_refctd_ptr converter = CAssetConverter::create({ .device = m_device.get() }); - const auto transferFamily = getTransferUpQueue()->getFamilyIndex(); - - struct SInputs : CAssetConverter::SInputs - { - virtual inline std::span getSharedOwnershipQueueFamilies(const size_t, const asset::ICPUBuffer*, const CAssetConverter::patch_t&) const - { - return sharedBufferOwnership; - } - - core::vector sharedBufferOwnership; - } inputs = {}; - core::vector> patches(cpuToConvert.size(), CSimpleDebugRenderer::DefaultPolygonGeometryPatch); - { - inputs.logger = m_logger.get(); - std::get>(inputs.assets) = { &cpuToConvert.front().get(),cpuToConvert.size() }; - std::get>(inputs.patches) = patches; - core::unordered_set families; - families.insert(transferFamily); - families.insert(getGraphicsQueue()->getFamilyIndex()); - if (families.size() > 1) - for (const auto fam : families) - inputs.sharedBufferOwnership.push_back(fam); - } - - auto reservation = converter->reserve(inputs); - if (!reservation) - failExit("Failed to reserve GPU objects for CPU->GPU conversion."); - - { - auto semaphore = m_device->createSemaphore(0u); - - constexpr auto MultiBuffering = 2; - std::array, MultiBuffering> commandBuffers = {}; - { - auto pool = m_device->createCommandPool(transferFamily, IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT | IGPUCommandPool::CREATE_FLAGS::TRANSIENT_BIT); - pool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, commandBuffers, smart_refctd_ptr(m_logger)); - } - commandBuffers.front()->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - - std::array commandBufferSubmits; - for (auto i = 0; i < MultiBuffering; i++) - commandBufferSubmits[i].cmdbuf = commandBuffers[i].get(); - - SIntendedSubmitInfo transfer = {}; - transfer.queue = getTransferUpQueue(); - transfer.scratchCommandBuffers = commandBufferSubmits; - transfer.scratchSemaphore = { - .semaphore = semaphore.get(), - .value = 0u, - .stageMask = PIPELINE_STAGE_FLAGS::ALL_TRANSFER_BITS - }; - - CAssetConverter::SConvertParams cpar = {}; - cpar.utilities = m_utils.get(); - cpar.transfer = &transfer; - - auto future = reservation.convert(cpar); - if (future.copy() != IQueue::RESULT::SUCCESS) - failExit("Failed to await submission feature."); - } - - const auto& converted = reservation.getGPUObjects(); - for (size_t i = 0u; i < converted.size(); ++i) - convertEntries[i]->gpu = converted[i]; - - stats.convertMs = toMs(clock_t::now() - convertStart); - } - - size_t existingCount = m_renderer->getGeometries().size(); - const bool incremental = (mode == RowViewReloadMode::Incremental) && (existingCount <= m_cases.size()); - if (!incremental && mode == RowViewReloadMode::Incremental) - return loadRowView(RowViewReloadMode::Full); - - if (mode == RowViewReloadMode::Full) - { - core::vector allGeometries; - allGeometries.reserve(m_cases.size()); - for (const auto& testCase : m_cases) - { - const auto& entry = m_rowViewCache[makeCacheKey(testCase.path)]; - if (!entry.gpu) - failExit("Missing GPU geometry for %s.", testCase.path.string().c_str()); - allGeometries.push_back(entry.gpu.get()); - } - stats.addCount = allGeometries.size(); - const auto addStart = clock_t::now(); - if (!allGeometries.empty()) - if (!m_renderer->addGeometries({ allGeometries.data(),allGeometries.size() })) - failExit("Failed to add geometries to renderer."); - stats.addGeoMs = toMs(clock_t::now() - addStart); - } - else - { - const size_t addCount = (existingCount < m_cases.size()) ? (m_cases.size() - existingCount) : 0u; - stats.addCount = addCount; - if (addCount > 0u) - { - core::vector newGeometries; - newGeometries.reserve(addCount); - for (size_t i = existingCount; i < m_cases.size(); ++i) - { - const auto& entry = m_rowViewCache[makeCacheKey(m_cases[i].path)]; - if (!entry.gpu) - failExit("Missing GPU geometry for %s.", m_cases[i].path.string().c_str()); - newGeometries.push_back(entry.gpu.get()); - } - const auto addStart = clock_t::now(); - if (!m_renderer->addGeometries({ newGeometries.data(),newGeometries.size() })) - failExit("Failed to add geometries to renderer."); - stats.addGeoMs = toMs(clock_t::now() - addStart); - } - } - - using aabb_t = hlsl::shapes::AABB<3, double>; - auto printAABB = [&](const aabb_t& aabb, const char* extraMsg = "")->void - { - m_logger->log("%s AABB is (%f,%f,%f) -> (%f,%f,%f)", ILogger::ELL_INFO, extraMsg, aabb.minVx.x, aabb.minVx.y, aabb.minVx.z, aabb.maxVx.x, aabb.maxVx.y, aabb.maxVx.z); - }; - auto bound = aabb_t::create(); - - const auto layoutStart = clock_t::now(); - double targetExtent = 0.0; - core::vector maxDims; - maxDims.reserve(aabbs.size()); - for (const auto& aabb : aabbs) - { - const auto extent = aabb.getExtent(); - const double maxDim = std::max({ extent.x, extent.y, extent.z, 0.001 }); - maxDims.push_back(maxDim); - if (maxDim > targetExtent) - targetExtent = maxDim; - } - - core::vector scales; - scales.reserve(aabbs.size()); - for (const auto maxDim : maxDims) - scales.push_back(targetExtent / maxDim); - - double maxWidth = 0.0; - double totalWidth = 0.0; - core::vector widths; - widths.reserve(aabbs.size()); - for (size_t i = 0; i < aabbs.size(); ++i) - { - const auto extent = aabbs[i].getExtent(); - const double width = std::max(0.001, extent.x * scales[i]); - widths.push_back(width); - totalWidth += width; - if (width > maxWidth) - maxWidth = width; - } - const double spacing = std::max(0.05 * maxWidth, 0.01); - const double totalSpan = totalWidth + spacing * double(widths.size() > 0 ? widths.size() - 1 : 0); - double cursor = -0.5 * totalSpan; - stats.layoutMs = toMs(clock_t::now() - layoutStart); - - const auto instanceStart = clock_t::now(); - auto tmp = hlsl::float32_t4x3( - hlsl::float32_t3(1, 0, 0), - hlsl::float32_t3(0, 1, 0), - hlsl::float32_t3(0, 0, 1), - hlsl::float32_t3(0, 0, 0) - ); - core::vector worldTforms; - worldTforms.reserve(geometries.size()); - m_aabbInstances.resize(geometries.size()); - if (m_drawBBMode == DBBM_OBB) - m_obbInstances.resize(geometries.size()); - m_renderer->m_instances.clear(); - - for (uint32_t i = 0; i < geometries.size(); i++) - { - const auto& cpuGeom = geometries[i].get(); - const auto aabb = aabbs[i]; - printAABB(aabb, "Geometry"); - - const double scale = scales[i]; - const auto center = (aabb.minVx + aabb.maxVx) * 0.5; - const double width = widths[i]; - const double targetCenterX = cursor + 0.5 * width; - cursor += width + spacing; - - const double tx = targetCenterX - scale * center.x; - const double ty = -scale * center.y; - const double tz = -scale * center.z; - tmp[0] = hlsl::float32_t3(static_cast(scale), 0.f, 0.f); - tmp[1] = hlsl::float32_t3(0.f, static_cast(scale), 0.f); - tmp[2] = hlsl::float32_t3(0.f, 0.f, static_cast(scale)); - tmp[3] = hlsl::float32_t3(static_cast(tx), static_cast(ty), static_cast(tz)); - - const auto promotedWorld = hlsl::float64_t3x4(worldTforms.emplace_back(hlsl::transpose(tmp))); - const auto translation = hlsl::float64_t3(tx, ty, tz); - const auto scaled = scaleAABB(aabb, scale); - const auto transformed = translateAABB(scaled, translation); - printAABB(transformed, "Transformed"); - bound = hlsl::shapes::util::union_(transformed, bound); - -#ifdef NBL_BUILD_DEBUG_DRAW - auto& aabbInst = m_aabbInstances[i]; - const auto tmpAabb = shapes::AABB<3, float>(aabb.minVx, aabb.maxVx); - hlsl::float32_t3x4 aabbTransform = ext::debug_draw::DrawAABB::getTransformFromAABB(tmpAabb); - const auto tmpWorld = hlsl::float32_t3x4(promotedWorld); - const auto world4x4 = float32_t4x4{ - tmpWorld[0], - tmpWorld[1], - tmpWorld[2], - float32_t4(0, 0, 0, 1) - }; - aabbInst.color = { 1,1,1,1 }; - aabbInst.transform = math::linalg::promoted_mul(world4x4, aabbTransform); - - if (m_drawBBMode == DBBM_OBB) - { - auto& obbInst = m_obbInstances[i]; - const auto obb = CPolygonGeometryManipulator::calculateOBB( - cpuGeom->getPositionView().getElementCount(), - [geo = cpuGeom](size_t vertex_i) { - hlsl::float32_t3 pt; - geo->getPositionView().decodeElement(vertex_i, pt); - return pt; - }); - obbInst.color = { 0, 0, 1, 1 }; - obbInst.transform = math::linalg::promoted_mul(world4x4, obb.transform); - } -#endif - } - - printAABB(bound, "Total"); - for (uint32_t i = 0; i < worldTforms.size(); i++) - { - m_renderer->m_instances.push_back({ - .world = worldTforms[i], - .packedGeo = &m_renderer->getGeometry(i) - }); - } - stats.instanceMs = toMs(clock_t::now() - instanceStart); - - const auto cameraStart = clock_t::now(); - setupCameraFromAABB(bound); - stats.cameraMs = toMs(clock_t::now() - cameraStart); - - m_modelPath = "Row view (all meshes)"; - m_rowViewScreenshotPath = m_screenshotPrefixPath / "meshloaders_row_view.png"; - m_rowViewScreenshotCaptured = false; - stats.totalMs = toMs(clock_t::now() - totalStart); - logRowViewPerf(stats); - return true; - } - - bool writeGeometry(smart_refctd_ptr geometry, const std::string& savePath) - { - using clock_t = std::chrono::high_resolution_clock; - const auto writeOuterStart = clock_t::now(); - IAsset* assetPtr = const_cast(static_cast(geometry.get())); - const auto ext = normalizeExtension(system::path(savePath)); - auto flags = asset::EWF_MESH_IS_RIGHT_HANDED; - if (ext != ".obj") - flags = static_cast(flags | asset::EWF_BINARY); - IAssetWriter::SAssetWriteParams params{ assetPtr, flags }; - params.logger = getAssetLoadLogger(); - m_logger->log("Saving mesh to %s", ILogger::ELL_INFO, savePath.c_str()); - const auto openStart = clock_t::now(); - system::ISystem::future_t> writeFileFuture; - m_system->createFile(writeFileFuture, system::path(savePath), system::IFile::ECF_WRITE); - core::smart_refctd_ptr writeFile; - writeFileFuture.acquire().move_into(writeFile); - const auto openMs = toMs(clock_t::now() - openStart); - if (!writeFile) - { - m_logger->log("Failed to open output file %s", ILogger::ELL_ERROR, savePath.c_str()); - return false; - } - const auto start = clock_t::now(); - if (!m_assetMgr->writeAsset(writeFile.get(), params)) - { - const auto ms = toMs(clock_t::now() - start); - m_logger->log("Failed to save %s after %.3f ms", ILogger::ELL_ERROR, savePath.c_str(), ms); - return false; - } - const auto writeMs = toMs(clock_t::now() - start); - const auto statStart = clock_t::now(); - uintmax_t size = 0u; - if (std::filesystem::exists(savePath)) - size = std::filesystem::file_size(savePath); - const auto statMs = toMs(clock_t::now() - statStart); - const auto outerMs = toMs(clock_t::now() - writeOuterStart); - const auto nonWriterMs = std::max(0.0, outerMs - writeMs); - m_logger->log("Asset write call perf: path=%s ext=%s time=%.3f ms size=%llu", ILogger::ELL_INFO, savePath.c_str(), ext.c_str(), writeMs, static_cast(size)); - m_logger->log( - "Asset write outer perf: path=%s ext=%s open=%.3f ms writeAsset=%.3f ms stat=%.3f ms total=%.3f ms non_writer=%.3f ms size=%llu", - ILogger::ELL_INFO, - savePath.c_str(), - ext.c_str(), - openMs, - writeMs, - statMs, - outerMs, - nonWriterMs, - static_cast(size)); - m_logger->log("Writer perf: path=%s ext=%s time=%.3f ms size=%llu", ILogger::ELL_INFO, savePath.c_str(), ext.c_str(), writeMs, static_cast(size)); - m_logger->log("Mesh successfully saved!", ILogger::ELL_INFO); - return true; - } - - void setupCameraFromAABB(const hlsl::shapes::AABB<3, double>& bound) - { - const auto extent = bound.getExtent(); - const auto aspectRatio = double(m_window->getWidth()) / double(m_window->getHeight()); - const double fovY = 1.2; - const double fovX = 2.0 * std::atan(std::tan(fovY * 0.5) * aspectRatio); - const auto center = (bound.minVx + bound.maxVx) * 0.5; - const auto halfExtent = extent * 0.5; - const double halfX = std::max(halfExtent.x, 0.001); - const double halfY = std::max(halfExtent.y, 0.001); - const double halfZ = std::max(halfExtent.z, 0.001); - const double safeRadius = std::max({ halfX, halfY, halfZ }); - - const double distY = halfY / std::tan(fovY * 0.5); - const double distX = halfX / std::tan(fovX * 0.5); - double dist = std::max(distX, distY) + halfZ; - dist *= 1.1; - - const auto dir = hlsl::float64_t3(0.0, 0.0, 1.0); - const auto pos = center + dir * dist; - - const double margin = halfZ * 0.1 + 0.01; - const double nearPlane = std::max(0.001, dist - halfZ - margin); - const double farPlane = dist + halfZ + margin; - - const auto projection = nbl::hlsl::buildProjectionMatrixPerspectiveFovRH( - static_cast(fovY), - static_cast(aspectRatio), - static_cast(nearPlane), - static_cast(farPlane)); - camera.setProjectionMatrix(projection); - camera.setMoveSpeed(static_cast(safeRadius * 0.1)); - camera.setPosition(vectorSIMDf(pos.x, pos.y, pos.z)); - camera.setTarget(vectorSIMDf(center.x, center.y, center.z)); - } - - static inline hlsl::shapes::AABB<3, double> translateAABB(const hlsl::shapes::AABB<3, double>& aabb, const hlsl::float64_t3& translation) - { - auto out = aabb; - out.minVx += translation; - out.maxVx += translation; - return out; - } - - static inline hlsl::shapes::AABB<3, double> scaleAABB(const hlsl::shapes::AABB<3, double>& aabb, const double scale) - { - auto out = aabb; - out.minVx *= scale; - out.maxVx *= scale; - return out; - } - - void storeCameraState() - { - m_referenceCamera = CameraState{ - camera.getPosition(), - camera.getTarget(), - camera.getProjectionMatrix(), - camera.getMoveSpeed() - }; - } - - void applyCameraState(const CameraState& state) - { - camera.setProjectionMatrix(state.projection); - camera.setPosition(state.position); - camera.setTarget(state.target); - camera.setMoveSpeed(state.moveSpeed); - } - - static bool isValidAABB(const hlsl::shapes::AABB<3, double>& aabb) - { - return - (aabb.minVx.x <= aabb.maxVx.x) && - (aabb.minVx.y <= aabb.maxVx.y) && - (aabb.minVx.z <= aabb.maxVx.z); - } - - hlsl::shapes::AABB<3, double> getGeometryAABB(const ICPUPolygonGeometry* geometry) const - { - if (!geometry) - return hlsl::shapes::AABB<3, double>::create(); - auto aabb = geometry->getAABB>(); - if (!isValidAABB(aabb)) - { - CPolygonGeometryManipulator::recomputeAABB(geometry); - aabb = geometry->getAABB>(); - } - return aabb; - } - - system::ILogger* getAssetLoadLogger() const - { - if (m_assetLoadLogger) - return m_assetLoadLogger.get(); - return m_logger.get(); - } - - IAssetLoader::SAssetLoadParams makeLoadParams() const - { - IAssetLoader::SAssetLoadParams params = {}; - params.logger = getAssetLoadLogger(); - if ((m_runMode == RunMode::CI || isRowViewActive()) && !m_loaderPerfLogger) - params.logger = nullptr; - params.cacheFlags = IAssetLoader::ECF_DUPLICATE_TOP_LEVEL; - params.ioPolicy.runtimeTuning.mode = m_runtimeTuningMode; - if (m_forceLoaderContentHashes) - params.loaderFlags = static_cast(params.loaderFlags | IAssetLoader::ELPF_COMPUTE_CONTENT_HASHES); - return params; - } - - struct AssetLoadCallResult - { - asset::SAssetBundle bundle = {}; - double openMs = 0.0; - double getAssetMs = 0.0; - uintmax_t inputSize = 0u; - unsigned fileFlags = 0u; - bool mapped = false; - }; - - bool loadAssetCallFromPath(const system::path& modelPath, const IAssetLoader::SAssetLoadParams& params, AssetLoadCallResult& out) - { - using clock_t = std::chrono::high_resolution_clock; - out.openMs = 0.0; - out.fileFlags = 0u; - out.mapped = false; - if (std::filesystem::exists(modelPath)) - out.inputSize = std::filesystem::file_size(modelPath); - else - out.inputSize = 0u; - - const auto loadStart = clock_t::now(); - out.bundle = m_assetMgr->getAsset(modelPath.string(), params); - out.getAssetMs = toMs(clock_t::now() - loadStart); - return true; - } - - bool initLoaderPerfLogger(const system::path& logPath) - { - if (!m_system) - return logFail("Could not initialize loader perf logger because system is unavailable."); - if (logPath.empty()) - return false; - const auto parent = logPath.parent_path(); - if (!parent.empty()) - { - std::error_code ec; - std::filesystem::create_directories(parent, ec); - if (ec) - return logFail("Could not create loader perf log directory %s", parent.string().c_str()); - } - system::ISystem::future_t> future; - m_system->createFile(future, logPath, system::IFile::ECF_READ_WRITE); - if (!future.wait() || !future.get()) - return logFail("Could not create loader perf log file %s", logPath.string().c_str()); - const auto logMask = core::bitflag(system::ILogger::ELL_ALL); - m_loaderPerfLogger = core::make_smart_refctd_ptr(future.copy(), false, logMask); - m_assetLoadLogger = m_loaderPerfLogger; - return true; - } - - std::string makeUniqueCaseName(const system::path& path) - { - auto base = path.stem().string(); - if (base.empty()) - base = "case"; - auto& counter = m_caseNameCounts[base]; - std::string name = (counter == 0u) ? base : (base + "_" + std::to_string(counter)); - ++counter; - return name; - } - - static double toMs(const std::chrono::high_resolution_clock::duration& d) - { - return std::chrono::duration(d).count(); - } - - std::string makeCacheKey(const system::path& path) const - { - return path.lexically_normal().generic_string(); - } - - void logRowViewPerf(const RowViewPerfStats& stats) const - { - if (!m_logger) - return; - m_logger->log( - "RowView perf: mode=%s cases=%llu cpuHit=%llu cpuMiss=%llu gpuHit=%llu gpuMiss=%llu convert=%llu add=%llu total=%.3f ms", - ILogger::ELL_INFO, - stats.incremental ? "inc" : "full", - static_cast(stats.cases), - static_cast(stats.cpuHits), - static_cast(stats.cpuMisses), - static_cast(stats.gpuHits), - static_cast(stats.gpuMisses), - static_cast(stats.convertCount), - static_cast(stats.addCount), - stats.totalMs); - m_logger->log( - "RowView perf: clear=%.3f load=%.3f extract=%.3f aabb=%.3f convert=%.3f add=%.3f layout=%.3f inst=%.3f cam=%.3f", - ILogger::ELL_INFO, - stats.clearMs, - stats.loadMs, - stats.extractMs, - stats.aabbMs, - stats.convertMs, - stats.addGeoMs, - stats.layoutMs, - stats.instanceMs, - stats.cameraMs); - } - - void logRowViewAssetLoad(const system::path& path, const double ms, const bool cached) const - { - if (!m_logger) - return; - m_logger->log( - "RowView perf: asset %s load=%.3f ms%s", - ILogger::ELL_INFO, - path.string().c_str(), - ms, - cached ? " (cached)" : ""); - } - - void logRowViewLoadTotal(const double ms, const size_t hits, const size_t misses) const - { - if (!m_logger) - return; - m_logger->log( - "RowView perf: asset load total=%.3f ms hits=%llu misses=%llu", - ILogger::ELL_INFO, - ms, - static_cast(hits), - static_cast(misses)); - } - - core::blake3_hash_t hashGeometry(const ICPUPolygonGeometry* geo) - { - return CPolygonGeometryManipulator::computeDeterministicContentHash(geo); - } - - struct GeometryCompareResult - { - uint64_t vertexCountA = 0u; - uint64_t vertexCountB = 0u; - bool hasNormalA = false; - bool hasNormalB = false; - bool hasUvA = false; - bool hasUvB = false; - uint64_t indexCountA = 0u; - uint64_t indexCountB = 0u; - uint64_t posDiffCount = 0u; - double posMaxAbs = 0.0; - uint64_t normalDiffCount = 0u; - double normalMaxAbs = 0.0; - uint64_t uvDiffCount = 0u; - double uvMaxAbs = 0.0; - uint64_t indexDiffCount = 0u; - }; - - const ICPUPolygonGeometry::SDataView* findUvView(const ICPUPolygonGeometry* geo) const - { - if (!geo) - return nullptr; - for (const auto& view : geo->getAuxAttributeViews()) - { - if (!view) - continue; - const auto channels = getFormatChannelCount(view.composed.format); - if (channels >= 2u) - return &view; - } - return nullptr; - } - - bool compareGeometry(const ICPUPolygonGeometry* a, const ICPUPolygonGeometry* b, const double tol, GeometryCompareResult& out) const - { - if (!a || !b) - return false; - - const auto& posA = a->getPositionView(); - const auto& posB = b->getPositionView(); - if (!posA || !posB) - return false; - - out.vertexCountA = posA.getElementCount(); - out.vertexCountB = posB.getElementCount(); - if (out.vertexCountA != out.vertexCountB) - return false; - - auto compareVec = [&](const ICPUPolygonGeometry::SDataView& viewA, const ICPUPolygonGeometry::SDataView& viewB, const uint32_t components, uint64_t& diffCount, double& maxAbs)->bool - { - hlsl::float32_t4 va = {}; - hlsl::float32_t4 vb = {}; - for (uint64_t i = 0; i < out.vertexCountA; ++i) - { - if (!viewA.decodeElement(i, va) || !viewB.decodeElement(i, vb)) - return false; - const float* aVals = &va.x; - const float* bVals = &vb.x; - for (uint32_t c = 0; c < components; ++c) - { - const double diff = std::abs(static_cast(aVals[c]) - static_cast(bVals[c])); - if (diff > maxAbs) - maxAbs = diff; - if (diff > tol) - ++diffCount; - } - } - return true; - }; - - if (!compareVec(posA, posB, 3u, out.posDiffCount, out.posMaxAbs)) - return false; - - const auto& normalA = a->getNormalView(); - const auto& normalB = b->getNormalView(); - out.hasNormalA = static_cast(normalA); - out.hasNormalB = static_cast(normalB); - if (out.hasNormalA != out.hasNormalB) - return false; - if (out.hasNormalA) - if (!compareVec(normalA, normalB, 3u, out.normalDiffCount, out.normalMaxAbs)) - return false; - - const auto* uvA = findUvView(a); - const auto* uvB = findUvView(b); - out.hasUvA = uvA != nullptr; - out.hasUvB = uvB != nullptr; - if (out.hasUvA != out.hasUvB) - return false; - if (out.hasUvA) - if (!compareVec(*uvA, *uvB, 2u, out.uvDiffCount, out.uvMaxAbs)) - return false; - - const auto& idxA = a->getIndexView(); - const auto& idxB = b->getIndexView(); - out.indexCountA = idxA ? idxA.getElementCount() : out.vertexCountA; - out.indexCountB = idxB ? idxB.getElementCount() : out.vertexCountB; - if (out.indexCountA != out.indexCountB) - return false; - - auto getIndex = [&](const ICPUPolygonGeometry::SDataView& view, const uint64_t ix)->uint32_t - { - const void* src = view.getPointer(); - if (!src) - return 0u; - if (view.composed.format == EF_R32_UINT) - return reinterpret_cast(src)[ix]; - if (view.composed.format == EF_R16_UINT) - return static_cast(reinterpret_cast(src)[ix]); - return 0u; - }; - - for (uint64_t i = 0; i < out.indexCountA; ++i) - { - const uint32_t aIdx = idxA ? getIndex(idxA, i) : static_cast(i); - const uint32_t bIdx = idxB ? getIndex(idxB, i) : static_cast(i); - if (aIdx != bIdx) - ++out.indexDiffCount; - } - - return out.posDiffCount == 0u && out.normalDiffCount == 0u && out.uvDiffCount == 0u && out.indexDiffCount == 0u; - } - - bool validateWrittenAsset(const system::path& path) - { - if (!std::filesystem::exists(path)) - return false; - - m_assetMgr->clearAllAssetCache(); - - IAssetLoader::SAssetLoadParams params = makeLoadParams(); - auto asset = m_assetMgr->getAsset(path.string(), params); - if (asset.getContents().empty()) - return false; - - core::vector> geometries; - switch (asset.getAssetType()) - { - case IAsset::E_TYPE::ET_GEOMETRY: - for (const auto& item : asset.getContents()) - if (auto polyGeo = IAsset::castDown(item); polyGeo) - geometries.push_back(polyGeo); - break; - default: - return false; - } - return !geometries.empty(); - } - - bool captureScreenshot(const system::path& path, core::smart_refctd_ptr& outImage) - { - if (!m_device || !m_surface || !m_assetMgr) - return false; - - m_device->waitIdle(); - - auto* scRes = static_cast(m_surface->getSwapchainResources()); - auto* fb = scRes ? scRes->getFramebuffer(device_base_t::getCurrentAcquire().imageIndex) : nullptr; - if (!fb) - return false; - - auto colorView = fb->getCreationParameters().colorAttachments[0u]; - if (!colorView) - return false; - - auto cpuView = ext::ScreenShot::createScreenShot( - m_device.get(), - getGraphicsQueue(), - nullptr, - colorView.get(), - asset::ACCESS_FLAGS::COLOR_ATTACHMENT_WRITE_BIT, - asset::IImage::LAYOUT::PRESENT_SRC); - if (!cpuView) - return false; - - if (!path.empty()) - std::filesystem::create_directories(path.parent_path()); - - IAssetWriter::SAssetWriteParams params(cpuView.get()); - if (!m_assetMgr->writeAsset(path.string(), params)) - return false; - - outImage = cpuView; - return true; - } - - bool appendGeometriesFromBundle(const asset::SAssetBundle& bundle, core::vector>& out) const - { - if (bundle.getContents().empty()) - return false; - - switch (bundle.getAssetType()) - { - case IAsset::E_TYPE::ET_GEOMETRY: - for (const auto& item : bundle.getContents()) - { - if (auto polyGeo = IAsset::castDown(item); polyGeo) - out.push_back(polyGeo); - } - break; - case IAsset::E_TYPE::ET_GEOMETRY_COLLECTION: - for (const auto& item : bundle.getContents()) - { - auto collection = IAsset::castDown(item); - if (!collection) - continue; - auto* refs = collection->getGeometries(); - if (!refs) - continue; - for (const auto& ref : *refs) - { - if (!ref.geometry) - continue; - if (ref.geometry->getPrimitiveType() != IGeometryBase::EPrimitiveType::Polygon) - continue; - auto poly = core::smart_refctd_ptr_static_cast(ref.geometry); - if (poly) - out.push_back(poly); - } - } - break; - default: - return false; - } - - return !out.empty(); - } - - bool compareImages(const asset::ICPUImageView* a, const asset::ICPUImageView* b, uint64_t& diffCount, uint8_t& maxDiff) - { - diffCount = 0u; - maxDiff = 0u; - if (!a || !b) - return false; - - const auto* imgA = a->getCreationParameters().image.get(); - const auto* imgB = b->getCreationParameters().image.get(); - if (!imgA || !imgB) - return false; - - const auto paramsA = imgA->getCreationParameters(); - const auto paramsB = imgB->getCreationParameters(); - if (paramsA.format != paramsB.format) - return false; - if (paramsA.extent != paramsB.extent) - return false; - - const auto* bufA = imgA->getBuffer(); - const auto* bufB = imgB->getBuffer(); - if (!bufA || !bufB) - return false; - - const size_t sizeA = bufA->getSize(); - if (sizeA != bufB->getSize()) - return false; - - const auto* dataA = static_cast(bufA->getPointer()); - const auto* dataB = static_cast(bufB->getPointer()); - if (!dataA || !dataB) - return false; - - for (size_t i = 0; i < sizeA; ++i) - { - const uint8_t va = dataA[i]; - const uint8_t vb = dataB[i]; - const uint8_t diff = va > vb ? static_cast(va - vb) : static_cast(vb - va); - if (diff) - { - ++diffCount; - if (diff > maxDiff) - maxDiff = diff; - } - } - - return true; - } - - void advanceCase() - { - if (m_runMode == RunMode::Interactive || m_cases.empty()) - return; - if (isRowViewActive()) - return; - - const uint32_t frameLimit = m_runMode == RunMode::CI ? CiFramesBeforeCapture : NonCiFramesPerCase; - ++m_phaseFrameCounter; - if (m_phaseFrameCounter < frameLimit) - return; - - if (m_phase == Phase::RenderOriginal) - { - if (!captureScreenshot(m_loadedScreenshotPath, m_loadedScreenshot)) - failExit("Failed to capture loaded screenshot."); - - if (m_saveGeom) - { - if (!m_currentCpuGeom) - failExit("No geometry to write."); - if (!writeGeometry(m_currentCpuGeom, m_writtenPath.string())) - failExit("Geometry write failed."); - } - - if (m_runMode == RunMode::CI) - { - if (!loadModel(m_writtenPath, false, false)) - failExit("Failed to load written asset %s.", m_writtenPath.string().c_str()); - if (!m_currentCpuGeom) - failExit("Written geometry missing."); - m_phase = Phase::RenderWritten; - m_phaseFrameCounter = 0u; - return; - } - - if (m_saveGeom) - { - if (!validateWrittenAsset(m_writtenPath)) - failExit("Failed to load written asset %s.", m_writtenPath.string().c_str()); - } - - advanceToNextCase(); - return; - } - - if (m_phase == Phase::RenderWritten) - { - if (!captureScreenshot(m_writtenScreenshotPath, m_writtenScreenshot)) - failExit("Failed to capture written screenshot."); - - if (m_hasReferenceGeometryHash) - { - const auto writtenHash = hashGeometry(m_currentCpuGeom.get()); - if (writtenHash != m_referenceGeometryHash) - { - m_logger->log("Geometry hash reference mismatch for %s. Current=%s Reference=%s ReferenceFile=%s", - ILogger::ELL_WARNING, - m_caseName.c_str(), - geometryHashToHex(writtenHash).c_str(), - geometryHashToHex(m_referenceGeometryHash).c_str(), - m_caseGeometryHashReferencePath.empty() ? "" : m_caseGeometryHashReferencePath.string().c_str()); - } - } - - if (m_hasReferenceGeometry) - { - GeometryCompareResult diff = {}; - const double tol = 1e-5; - if (!compareGeometry(m_referenceCpuGeom.get(), m_currentCpuGeom.get(), tol, diff)) - { - m_logger->log("Geometry compare failed for %s. Vtx(%llu vs %llu) Idx(%llu vs %llu) PosDiff(%llu max %.8f) NDiff(%llu max %.8f) UvDiff(%llu max %.8f) IdxDiff(%llu) Normals(%d/%d) UV(%d/%d)", - ILogger::ELL_ERROR, - m_caseName.c_str(), - static_cast(diff.vertexCountA), - static_cast(diff.vertexCountB), - static_cast(diff.indexCountA), - static_cast(diff.indexCountB), - static_cast(diff.posDiffCount), - diff.posMaxAbs, - static_cast(diff.normalDiffCount), - diff.normalMaxAbs, - static_cast(diff.uvDiffCount), - diff.uvMaxAbs, - static_cast(diff.indexDiffCount), - diff.hasNormalA ? 1 : 0, - diff.hasNormalB ? 1 : 0, - diff.hasUvA ? 1 : 0, - diff.hasUvB ? 1 : 0); - failExit("Geometry compare failed for %s.", m_caseName.c_str()); - } - } - - uint64_t diffCount = 0u; - uint8_t maxDiff = 0u; - if (!compareImages(m_loadedScreenshot.get(), m_writtenScreenshot.get(), diffCount, maxDiff)) - failExit("Image compare failed for %s.", m_caseName.c_str()); - if (diffCount > MaxImageDiffBytes || maxDiff > MaxImageDiffValue) - failExit("Image diff detected for %s. Bytes: %llu MaxDiff: %u", m_caseName.c_str(), static_cast(diffCount), maxDiff); - if (diffCount != 0u) - m_logger->log("Image diff within tolerance for %s. Bytes: %llu MaxDiff: %u", ILogger::ELL_WARNING, m_caseName.c_str(), static_cast(diffCount), maxDiff); - - advanceToNextCase(); - } - } - - // Maximum frames which can be simultaneously submitted, used to cycle through our per-frame resources like command buffers - constexpr static inline uint32_t MaxFramesInFlight = 3u; - constexpr static inline uint32_t CiFramesBeforeCapture = 10u; - constexpr static inline uint32_t NonCiFramesPerCase = 120u; - constexpr static inline uint32_t RowViewFramesBeforeCapture = 10u; - constexpr static inline uint64_t MaxImageDiffBytes = 16u; - constexpr static inline uint8_t MaxImageDiffValue = 1u; - // - smart_refctd_ptr m_renderer; - // - smart_refctd_ptr m_semaphore; - uint64_t m_realFrameIx = 0; - std::array, MaxFramesInFlight> m_cmdBufs; - // - InputSystem::ChannelReader mouse; - InputSystem::ChannelReader keyboard; - // - Camera camera = Camera( - core::vectorSIMDf(0, 0, 0), - core::vectorSIMDf(0, 0, -1), - nbl::hlsl::math::linalg::diagonal(1.0f)); - // mutables - std::string m_modelPath; - std::string m_caseName; - - DrawBoundingBoxMode m_drawBBMode = DBBM_AABB; -#ifdef NBL_BUILD_DEBUG_DRAW - smart_refctd_ptr m_drawAABB; - std::vector m_aabbInstances; - std::vector m_obbInstances; - -#endif - - bool m_saveGeom = true; - std::optional m_specifiedGeomSavePath; - nbl::system::path m_saveGeomPrefixPath; - nbl::system::path m_screenshotPrefixPath; - nbl::system::path m_rowViewScreenshotPath; - nbl::system::path m_testListPath; - nbl::system::path m_geometryHashReferenceDir; - nbl::system::path m_caseGeometryHashReferencePath; - std::optional m_loaderPerfLogPath; - std::optional m_rowAddPath; - uint32_t m_rowDuplicateCount = 0u; - smart_refctd_ptr m_assetLoadLogger; - smart_refctd_ptr m_loaderPerfLogger; - bool m_updateGeometryHashReferences = false; - bool m_forceLoaderContentHashes = true; - asset::SFileIOPolicy::SRuntimeTuning::Mode m_runtimeTuningMode = asset::SFileIOPolicy::SRuntimeTuning::Mode::Heuristic; - - RunMode m_runMode = RunMode::Batch; - Phase m_phase = Phase::RenderOriginal; - uint32_t m_phaseFrameCounter = 0u; - size_t m_caseIndex = 0u; - core::vector m_cases; - std::unordered_map m_caseNameCounts; - std::unordered_map m_rowViewCache; - bool m_shouldQuit = false; - - nbl::system::path m_writtenPath; - nbl::system::path m_loadedScreenshotPath; - nbl::system::path m_writtenScreenshotPath; - - core::smart_refctd_ptr m_currentCpuGeom; - core::smart_refctd_ptr m_referenceCpuGeom; - bool m_hasReferenceGeometry = false; - core::blake3_hash_t m_referenceGeometryHash = {}; - bool m_hasReferenceGeometryHash = false; - - core::smart_refctd_ptr m_loadedScreenshot; - core::smart_refctd_ptr m_writtenScreenshot; - - std::optional m_referenceCamera; -}; +#include "MeshLoadersApp.hpp" NBL_MAIN_FUNC(MeshLoadersApp) From c9a8735e85a20d18b406daa7980eef119cb91bb5 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Thu, 12 Feb 2026 19:17:04 +0100 Subject: [PATCH 08/34] Refactor meshloaders app flow and benchmark CTest wiring --- 12_MeshLoaders/CMakeLists.txt | 64 +++++++++++++++++++ 12_MeshLoaders/MeshLoadersApp.hpp | 7 +- 12_MeshLoaders/MeshLoadersAppLifecycle.cpp | 2 +- 12_MeshLoaders/MeshLoadersAppLoad.cpp | 45 +++++++++---- 12_MeshLoaders/MeshLoadersAppRuntime.cpp | 2 +- 12_MeshLoaders/main.cpp | 2 +- .../examples/common/MonoWindowApplication.hpp | 2 +- 7 files changed, 107 insertions(+), 17 deletions(-) diff --git a/12_MeshLoaders/CMakeLists.txt b/12_MeshLoaders/CMakeLists.txt index d45fcba50..b4f33d195 100644 --- a/12_MeshLoaders/CMakeLists.txt +++ b/12_MeshLoaders/CMakeLists.txt @@ -8,6 +8,11 @@ set(SRCs README.md ) +option(NBL_MESHLOADERS_ENABLE_BENCHMARK_DATASETS "Enable benchmark dataset clone + benchmark payload setup for 12_MeshLoaders." OFF) +set(NBL_MESHLOADERS_BENCHMARK_DATASET_DIR "${CMAKE_BINARY_DIR}/meshloaders_benchmark_datasets" CACHE PATH "Destination directory for cloned 12_MeshLoaders benchmark datasets.") +set(NBL_MESHLOADERS_BENCHMARK_DATASET_REPO "https://github.com/Devsh-Graphics-Programming/Nabla-Benchmark-Datasets.git" CACHE STRING "Git repository URL for 12_MeshLoaders benchmark datasets.") +set(NBL_MESHLOADERS_BENCHMARK_PAYLOAD_RELATIVE_PATH "inputs_benchmark.json" CACHE STRING "Relative path to committed benchmark payload JSON inside dataset repo.") + set(NBL_INCLUDE_SEARCH_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}/include" "${CMAKE_SOURCE_DIR}/3rdparty" @@ -38,6 +43,38 @@ target_include_directories(${EXECUTABLE_NAME} PUBLIC $) +if (NBL_MESHLOADERS_ENABLE_BENCHMARK_DATASETS) + set(NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON "${NBL_MESHLOADERS_BENCHMARK_DATASET_DIR}/${NBL_MESHLOADERS_BENCHMARK_PAYLOAD_RELATIVE_PATH}" CACHE FILEPATH "Committed benchmark testlist for 12_MeshLoaders." FORCE) + if (NOT EXISTS "${NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON}") + if (EXISTS "${NBL_MESHLOADERS_BENCHMARK_DATASET_DIR}" AND NOT EXISTS "${NBL_MESHLOADERS_BENCHMARK_DATASET_DIR}/.git") + message(STATUS "[meshloaders-bench] Dataset dir exists without .git, skipping fetch and using local files: ${NBL_MESHLOADERS_BENCHMARK_DATASET_DIR}") + else() + include(FetchContent) + FetchContent_Declare(nbl_meshloaders_benchmark_dataset + GIT_REPOSITORY "${NBL_MESHLOADERS_BENCHMARK_DATASET_REPO}" + GIT_TAG "master" + GIT_SHALLOW TRUE + GIT_PROGRESS TRUE + SOURCE_DIR "${NBL_MESHLOADERS_BENCHMARK_DATASET_DIR}" + BINARY_DIR "${CMAKE_BINARY_DIR}/CMakeFiles/nbl_meshloaders_benchmark_dataset-build" + ) + FetchContent_GetProperties(nbl_meshloaders_benchmark_dataset) + if (NOT nbl_meshloaders_benchmark_dataset_POPULATED) + FetchContent_Populate(nbl_meshloaders_benchmark_dataset) + endif () + endif () + endif () + if (NOT EXISTS "${NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON}") + message(FATAL_ERROR "Benchmark payload JSON not found: ${NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON}. Commit a full payload file into dataset repo and point NBL_MESHLOADERS_BENCHMARK_PAYLOAD_RELATIVE_PATH to it.") + endif () + file(READ "${NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON}" _meshloaders_payload_probe LIMIT 256) + string(FIND "${_meshloaders_payload_probe}" "version https://git-lfs.github.com/spec/v1" _meshloaders_payload_lfs_ix) + if (NOT _meshloaders_payload_lfs_ix EQUAL -1) + message(FATAL_ERROR "Benchmark payload JSON must be a normal Git file, not an LFS pointer: ${NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON}") + endif () + message(STATUS "[meshloaders-bench] Benchmark inputs payload: ${NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON}") +endif() + enable_testing() add_test(NAME NBL_MESHLOADERS_CI @@ -45,3 +82,30 @@ add_test(NAME NBL_MESHLOADERS_CI WORKING_DIRECTORY "$" COMMAND_EXPAND_LISTS ) + +if (NBL_MESHLOADERS_ENABLE_BENCHMARK_DATASETS) + set(_NBL_MESHLOADERS_BENCHMARK_CI_COMMON_ARGS + --ci + --update-references + --testlist "${NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON}" + --loader-content-hashes + ) + + add_test(NAME NBL_MESHLOADERS_CI_BENCHMARK_HEURISTIC + COMMAND "$" ${_NBL_MESHLOADERS_BENCHMARK_CI_COMMON_ARGS} --runtime-tuning heuristic + WORKING_DIRECTORY "$" + COMMAND_EXPAND_LISTS + ) + add_test(NAME NBL_MESHLOADERS_CI_BENCHMARK_HYBRID + COMMAND "$" ${_NBL_MESHLOADERS_BENCHMARK_CI_COMMON_ARGS} --runtime-tuning hybrid + WORKING_DIRECTORY "$" + COMMAND_EXPAND_LISTS + ) + set_tests_properties( + NBL_MESHLOADERS_CI_BENCHMARK_HEURISTIC + NBL_MESHLOADERS_CI_BENCHMARK_HYBRID + PROPERTIES + TIMEOUT 21600 + LABELS "meshloaders;benchmark;ci" + ) +endif() diff --git a/12_MeshLoaders/MeshLoadersApp.hpp b/12_MeshLoaders/MeshLoadersApp.hpp index e70666d00..49627878e 100644 --- a/12_MeshLoaders/MeshLoadersApp.hpp +++ b/12_MeshLoaders/MeshLoadersApp.hpp @@ -1,7 +1,7 @@ #ifndef _NBL_EXAMPLES_12_MESHLOADERS_APP_H_INCLUDED_ #define _NBL_EXAMPLES_12_MESHLOADERS_APP_H_INCLUDED_ -// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// Copyright (C) 2018-2025 - DevSH Graphics Programming Sp. z O.O. // This file is part of the "Nabla Engine". // For conditions of distribution and use, see copyright notice in nabla.h @@ -109,6 +109,11 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc bool keepRunning() override; protected: + core::bitflag getLogLevelMask() override + { + return system::ILogger::DefaultLogMask() | system::ILogger::ELL_INFO; + } + const video::IGPURenderpass::SCreationParams::SSubpassDependency* getDefaultSubpassDependencies() const override; private: diff --git a/12_MeshLoaders/MeshLoadersAppLifecycle.cpp b/12_MeshLoaders/MeshLoadersAppLifecycle.cpp index b290d68ab..5d0af0e41 100644 --- a/12_MeshLoaders/MeshLoadersAppLifecycle.cpp +++ b/12_MeshLoaders/MeshLoadersAppLifecycle.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// Copyright (C) 2018-2025 - DevSH Graphics Programming Sp. z O.O. // This file is part of the "Nabla Engine". // For conditions of distribution and use, see copyright notice in nabla.h diff --git a/12_MeshLoaders/MeshLoadersAppLoad.cpp b/12_MeshLoaders/MeshLoadersAppLoad.cpp index 24f1ce29a..a91fc9050 100644 --- a/12_MeshLoaders/MeshLoadersAppLoad.cpp +++ b/12_MeshLoaders/MeshLoadersAppLoad.cpp @@ -1,9 +1,10 @@ -// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// Copyright (C) 2018-2025 - DevSH Graphics Programming Sp. z O.O. // This file is part of the "Nabla Engine". // For conditions of distribution and use, see copyright notice in nabla.h #include "MeshLoadersApp.hpp" +#include #include #include @@ -660,17 +661,36 @@ void MeshLoadersApp::setupCameraFromAABB(const hlsl::shapes::AABB<3, double>& bo const double halfZ = std::max(halfExtent.z, 0.001); const double safeRadius = std::max({ halfX, halfY, halfZ }); - const double distY = halfY / std::tan(fovY * 0.5); - const double distX = halfX / std::tan(fovX * 0.5); - double dist = std::max(distX, distY) + halfZ; - dist *= 1.1; + struct CameraCandidate + { + hlsl::float64_t3 dir; + double planeHalfX; + double planeHalfY; + double depthHalf; + double footprint; + }; + std::array candidates = { + CameraCandidate{ hlsl::float64_t3(1.0, 0.0, 0.0), halfY, halfZ, halfX, halfY * halfZ }, + CameraCandidate{ hlsl::float64_t3(0.0, 1.0, 0.0), halfX, halfZ, halfY, halfX * halfZ }, + CameraCandidate{ hlsl::float64_t3(0.0, 0.0, 1.0), halfX, halfY, halfZ, halfX * halfY } + }; + const auto bestIt = std::max_element(candidates.begin(), candidates.end(), [](const CameraCandidate& a, const CameraCandidate& b) { return a.footprint < b.footprint; }); + const CameraCandidate best = (bestIt != candidates.end()) ? *bestIt : candidates[2u]; + + const double distY = best.planeHalfY / std::tan(fovY * 0.5); + const double distX = best.planeHalfX / std::tan(fovX * 0.5); + const double framingMargin = std::max(0.1, safeRadius * 0.35); + const double dist = std::max(distX, distY) + best.depthHalf + framingMargin; - const auto dir = hlsl::float64_t3(0.0, 0.0, 1.0); + const auto dir = best.dir; const auto pos = center + dir * dist; - const double margin = halfZ * 0.1 + 0.01; - const double nearPlane = std::max(0.001, dist - halfZ - margin); - const double farPlane = dist + halfZ + margin; + const double tightNear = std::max(0.0, dist - best.depthHalf - framingMargin); + const double tightFar = dist + best.depthHalf + framingMargin; + const double nearByTight = tightNear * 0.01; + const double nearByRadius = safeRadius * 0.002; + const double nearPlane = std::max(0.001, std::min({ nearByTight, nearByRadius, 1.0 })); + const double farPlane = std::max({ tightFar * 16.0, nearPlane + safeRadius * 24.0 + 10.0, dist + safeRadius * 24.0 }); const auto projection = nbl::hlsl::buildProjectionMatrixPerspectiveFovRH( static_cast(fovY), @@ -678,7 +698,8 @@ void MeshLoadersApp::setupCameraFromAABB(const hlsl::shapes::AABB<3, double>& bo static_cast(nearPlane), static_cast(farPlane)); camera.setProjectionMatrix(projection); - camera.setMoveSpeed(static_cast(safeRadius * 0.1)); + const double moveSpeed = std::clamp(safeRadius * 0.015, 0.2, 40.0); + camera.setMoveSpeed(static_cast(moveSpeed)); camera.setPosition(vectorSIMDf(pos.x, pos.y, pos.z)); camera.setTarget(vectorSIMDf(center.x, center.y, center.z)); } @@ -753,8 +774,8 @@ IAssetLoader::SAssetLoadParams MeshLoadersApp::makeLoadParams() const params.logger = nullptr; params.cacheFlags = IAssetLoader::ECF_DUPLICATE_TOP_LEVEL; params.ioPolicy.runtimeTuning.mode = m_runtimeTuningMode; - if (m_forceLoaderContentHashes) - params.loaderFlags = static_cast(params.loaderFlags | IAssetLoader::ELPF_COMPUTE_CONTENT_HASHES); + if (!m_forceLoaderContentHashes) + params.loaderFlags = static_cast(params.loaderFlags | IAssetLoader::ELPF_DONT_COMPUTE_CONTENT_HASHES); return params; } diff --git a/12_MeshLoaders/MeshLoadersAppRuntime.cpp b/12_MeshLoaders/MeshLoadersAppRuntime.cpp index 362981020..260b73334 100644 --- a/12_MeshLoaders/MeshLoadersAppRuntime.cpp +++ b/12_MeshLoaders/MeshLoadersAppRuntime.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// Copyright (C) 2018-2025 - DevSH Graphics Programming Sp. z O.O. // This file is part of the "Nabla Engine". // For conditions of distribution and use, see copyright notice in nabla.h diff --git a/12_MeshLoaders/main.cpp b/12_MeshLoaders/main.cpp index 72f8980a2..ee9ab7ef4 100644 --- a/12_MeshLoaders/main.cpp +++ b/12_MeshLoaders/main.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// Copyright (C) 2018-2025 - DevSH Graphics Programming Sp. z O.O. // This file is part of the "Nabla Engine". // For conditions of distribution and use, see copyright notice in nabla.h diff --git a/common/include/nbl/examples/common/MonoWindowApplication.hpp b/common/include/nbl/examples/common/MonoWindowApplication.hpp index 59c7ece65..658b4146b 100644 --- a/common/include/nbl/examples/common/MonoWindowApplication.hpp +++ b/common/include/nbl/examples/common/MonoWindowApplication.hpp @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2023 - DevSH Graphics Programming Sp. z O.O. +// Copyright (C) 2018-2025 - DevSH Graphics Programming Sp. z O.O. // This file is part of the "Nabla Engine". // For conditions of distribution and use, see copyright notice in nabla.h #ifndef _NBL_EXAMPLES_COMMON_MONO_WINDOW_APPLICATION_HPP_INCLUDED_ From 07224bdb448fd5be5659fdc79fb06d28de0a1144 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Fri, 13 Feb 2026 13:23:05 +0100 Subject: [PATCH 09/34] Improve meshloaders row view startup and controls --- 12_MeshLoaders/CMakeLists.txt | 8 + 12_MeshLoaders/MeshLoadersApp.hpp | 2 + 12_MeshLoaders/MeshLoadersAppLifecycle.cpp | 170 +++++++++++++++++++-- 12_MeshLoaders/MeshLoadersAppLoad.cpp | 131 ++++++++++++---- 12_MeshLoaders/README.md | 29 ++++ 5 files changed, 302 insertions(+), 38 deletions(-) diff --git a/12_MeshLoaders/CMakeLists.txt b/12_MeshLoaders/CMakeLists.txt index b4f33d195..67c82bfa4 100644 --- a/12_MeshLoaders/CMakeLists.txt +++ b/12_MeshLoaders/CMakeLists.txt @@ -9,6 +9,7 @@ set(SRCs ) option(NBL_MESHLOADERS_ENABLE_BENCHMARK_DATASETS "Enable benchmark dataset clone + benchmark payload setup for 12_MeshLoaders." OFF) +option(NBL_MESHLOADERS_DEFAULT_START_WITH_BENCHMARK_TESTLIST "When benchmark datasets are enabled, use benchmark payload as default startup test list in batch mode." OFF) set(NBL_MESHLOADERS_BENCHMARK_DATASET_DIR "${CMAKE_BINARY_DIR}/meshloaders_benchmark_datasets" CACHE PATH "Destination directory for cloned 12_MeshLoaders benchmark datasets.") set(NBL_MESHLOADERS_BENCHMARK_DATASET_REPO "https://github.com/Devsh-Graphics-Programming/Nabla-Benchmark-Datasets.git" CACHE STRING "Git repository URL for 12_MeshLoaders benchmark datasets.") set(NBL_MESHLOADERS_BENCHMARK_PAYLOAD_RELATIVE_PATH "inputs_benchmark.json" CACHE STRING "Relative path to committed benchmark payload JSON inside dataset repo.") @@ -73,6 +74,13 @@ if (NBL_MESHLOADERS_ENABLE_BENCHMARK_DATASETS) message(FATAL_ERROR "Benchmark payload JSON must be a normal Git file, not an LFS pointer: ${NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON}") endif () message(STATUS "[meshloaders-bench] Benchmark inputs payload: ${NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON}") + if (NBL_MESHLOADERS_DEFAULT_START_WITH_BENCHMARK_TESTLIST) + file(TO_CMAKE_PATH "${NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON}" _NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON_CMAKE) + target_compile_definitions(${EXECUTABLE_NAME} PRIVATE NBL_MESHLOADERS_DEFAULT_BENCHMARK_TESTLIST_PATH="${_NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON_CMAKE}") + message(STATUS "[meshloaders-bench] Default batch startup test list: benchmark payload") + else() + message(STATUS "[meshloaders-bench] Default batch startup test list: local inputs.json") + endif() endif() enable_testing() diff --git a/12_MeshLoaders/MeshLoadersApp.hpp b/12_MeshLoaders/MeshLoadersApp.hpp index 49627878e..7a57002e5 100644 --- a/12_MeshLoaders/MeshLoadersApp.hpp +++ b/12_MeshLoaders/MeshLoadersApp.hpp @@ -142,6 +142,7 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc bool addRowViewCase(); bool addRowViewCaseFromPath(const system::path& picked); bool reloadFromTestList(); + void resetRowViewScene(); bool loadModel(const system::path& modelPath, bool updateCamera, bool storeCamera); bool loadRowView(RowViewReloadMode mode); @@ -210,6 +211,7 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc bool m_nonInteractiveTest = false; bool m_rowViewEnabled = true; + bool m_forceRowViewForCurrentTestList = false; bool m_rowViewScreenshotCaptured = false; bool m_fileDialogOpen = false; diff --git a/12_MeshLoaders/MeshLoadersAppLifecycle.cpp b/12_MeshLoaders/MeshLoadersAppLifecycle.cpp index 5d0af0e41..82b24128e 100644 --- a/12_MeshLoaders/MeshLoadersAppLifecycle.cpp +++ b/12_MeshLoaders/MeshLoadersAppLifecycle.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #ifdef NBL_BUILD_MITSUBA_LOADER @@ -20,6 +21,99 @@ #include "nbl/system/CFileLogger.h" +namespace +{ + +template +std::string makeCaptionModelPath(const std::string& modelPath, const ArgContainer& argv) +{ + if (modelPath.empty()) + return {}; + + std::error_code ec; + if (modelPath.find('/') == std::string::npos && modelPath.find('\\') == std::string::npos) + { + if (!std::filesystem::exists(std::filesystem::path(modelPath), ec)) + { + ec.clear(); + return modelPath; + } + ec.clear(); + } + std::filesystem::path targetPath(modelPath); + targetPath = targetPath.lexically_normal(); + const auto canonicalTarget = std::filesystem::weakly_canonical(targetPath, ec); + if (!ec) + targetPath = canonicalTarget; + else + ec.clear(); + + if (!targetPath.is_absolute()) + { + const auto absoluteTarget = std::filesystem::absolute(targetPath, ec); + if (!ec) + targetPath = absoluteTarget.lexically_normal(); + else + ec.clear(); + } + if (!targetPath.is_absolute()) + return targetPath.generic_string(); + + auto relativeFromBase = [&](const std::filesystem::path& basePath) -> std::string + { + if (basePath.empty()) + return {}; + auto canonicalBase = std::filesystem::weakly_canonical(basePath, ec); + if (ec) + { + ec.clear(); + canonicalBase = std::filesystem::absolute(basePath, ec); + } + if (ec) + { + ec.clear(); + return {}; + } + const auto relativePath = std::filesystem::relative(targetPath, canonicalBase, ec); + if (ec || relativePath.empty() || relativePath.is_absolute()) + { + ec.clear(); + return {}; + } + return relativePath.lexically_normal().generic_string(); + }; + + std::string bestRelativePath; + if (!argv.empty() && !argv[0].empty()) + { + const auto exePath = std::filesystem::absolute(std::filesystem::path(argv[0]), ec); + if (!ec) + { + const auto relativeToExe = relativeFromBase(exePath.parent_path()); + if (!relativeToExe.empty()) + bestRelativePath = relativeToExe; + } + else + ec.clear(); + } + + const auto cwd = std::filesystem::current_path(ec); + if (!ec) + { + const auto relativeToCwd = relativeFromBase(cwd); + if (!relativeToCwd.empty() && (bestRelativePath.empty() || relativeToCwd.size() < bestRelativePath.size())) + bestRelativePath = relativeToCwd; + } + else + ec.clear(); + + if (!bestRelativePath.empty()) + return bestRelativePath; + return targetPath.generic_string(); +} + +} + MeshLoadersApp::MeshLoadersApp( const path& localInputCWD, const path& localOutputCWD, @@ -40,10 +134,25 @@ bool MeshLoadersApp::onAppInitialized(smart_refctd_ptr&& system) if (!device_base_t::onAppInitialized(smart_refctd_ptr(system))) return false; + const auto resolveRuntimeCWD = [](const path& preferred)->path + { + if (preferred.empty() || preferred == path("/") || preferred == path("\\")) + return path(std::filesystem::current_path()); + return preferred; + }; + const path effectiveInputCWD = resolveRuntimeCWD(localInputCWD); + const path effectiveOutputCWD = resolveRuntimeCWD(localOutputCWD); + m_runMode = RunMode::Batch; - m_saveGeomPrefixPath = localOutputCWD / "saved"; - m_screenshotPrefixPath = localOutputCWD / "screenshots"; - m_testListPath = localInputCWD / "inputs.json"; + m_saveGeomPrefixPath = effectiveOutputCWD / "saved"; + m_screenshotPrefixPath = effectiveOutputCWD / "screenshots"; + m_testListPath = effectiveInputCWD / "inputs.json"; + m_forceRowViewForCurrentTestList = false; +#if defined(NBL_MESHLOADERS_DEFAULT_BENCHMARK_TESTLIST_PATH) + const path defaultBenchmarkTestListPath = path(NBL_MESHLOADERS_DEFAULT_BENCHMARK_TESTLIST_PATH); +#else + const path defaultBenchmarkTestListPath; +#endif argparse::ArgumentParser parser("12_meshloaders"); parser.add_argument("--savegeometry") @@ -96,6 +205,7 @@ bool MeshLoadersApp::onAppInitialized(smart_refctd_ptr&& system) m_runMode = RunMode::Interactive; if (parser["--ci"] == true) m_runMode = RunMode::CI; + const bool hasExplicitTestListArg = parser.present("--testlist").has_value(); if (parser.present("--savepath")) { @@ -110,20 +220,30 @@ bool MeshLoadersApp::onAppInitialized(smart_refctd_ptr&& system) m_specifiedGeomSavePath.emplace(std::move(tmp.generic_string())); } - if (parser.present("--testlist")) + if (hasExplicitTestListArg) { auto tmp = path(parser.get("--testlist")); if (tmp.empty()) return logFail("Invalid path has been specified in --testlist argument"); if (tmp.is_relative()) - tmp = localInputCWD / tmp; + tmp = effectiveInputCWD / tmp; m_testListPath = tmp; } + else if (m_runMode == RunMode::Batch && !defaultBenchmarkTestListPath.empty()) + { + std::error_code benchmarkPathEc; + if (std::filesystem::exists(defaultBenchmarkTestListPath, benchmarkPathEc) && !benchmarkPathEc) + { + m_testListPath = defaultBenchmarkTestListPath; + m_forceRowViewForCurrentTestList = true; + m_logger->log("Using benchmark test list for default batch startup: %s", ILogger::ELL_INFO, m_testListPath.string().c_str()); + } + } if (parser.present("--row-add")) { auto tmp = path(parser.get("--row-add")); if (tmp.is_relative()) - tmp = localInputCWD / tmp; + tmp = effectiveInputCWD / tmp; m_rowAddPath = tmp; } if (parser.present("--row-duplicate")) @@ -144,7 +264,7 @@ bool MeshLoadersApp::onAppInitialized(smart_refctd_ptr&& system) if (tmp.empty()) return logFail("Invalid --loader-perf-log value."); if (tmp.is_relative()) - tmp = localOutputCWD / tmp; + tmp = effectiveOutputCWD / tmp; m_loaderPerfLogPath = tmp; } if (parser["--update-references"] == true) @@ -165,8 +285,8 @@ bool MeshLoadersApp::onAppInitialized(smart_refctd_ptr&& system) return logFail("Invalid --runtime-tuning value. Expected: none|heuristic|hybrid."); } - const path inputReferencesDir = localInputCWD / "references"; - const path outputReferencesDir = localOutputCWD / "references"; + const path inputReferencesDir = effectiveInputCWD / "references"; + const path outputReferencesDir = effectiveOutputCWD / "references"; std::error_code referenceDirEc; const bool hasInputReferencesDir = std::filesystem::is_directory(inputReferencesDir, referenceDirEc) && !referenceDirEc; referenceDirEc.clear(); @@ -306,6 +426,7 @@ IQueue::SSubmitInfo::SSemaphoreInfo MeshLoadersApp::renderFrame(const std::chron bool reloadInteractiveRequested = false; bool reloadListRequested = false; bool addRowViewRequested = false; + bool clearRowViewRequested = false; camera.beginInputProcessing(nextPresentationTimestamp); mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void { camera.mouseProcess(events); }, m_logger.get()); keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t& events) -> void @@ -326,12 +447,19 @@ IQueue::SSubmitInfo::SSemaphoreInfo MeshLoadersApp::renderFrame(const std::chron if (isRowViewActive()) addRowViewRequested = true; } + else if (event.keyCode == E_KEY_CODE::EKC_X) + { + if (isRowViewActive()) + clearRowViewRequested = true; + } } camera.keyboardProcess(events); }, m_logger.get() ); camera.endInputProcessing(nextPresentationTimestamp); + if (clearRowViewRequested) + resetRowViewScene(); if (addRowViewRequested) addRowViewCase(); if (reloadListRequested) @@ -396,7 +524,7 @@ IQueue::SSubmitInfo::SSemaphoreInfo MeshLoadersApp::renderFrame(const std::chron std::string caption = "[Nabla Engine] Mesh Loaders"; { caption += ", displaying ["; - caption += m_modelPath; + caption += makeCaptionModelPath(m_modelPath, argv); caption += "]"; m_window->setCaption(caption); } @@ -528,6 +656,7 @@ bool MeshLoadersApp::loadTestList(const system::path& jsonPath) { if (!std::filesystem::exists(jsonPath)) return logFail("Missing test list: %s", jsonPath.string().c_str()); + m_rowViewEnabled = true; std::ifstream stream(jsonPath); if (!stream.is_open()) @@ -554,6 +683,8 @@ bool MeshLoadersApp::loadTestList(const system::path& jsonPath) return logFail("\"row_view\" must be a boolean."); m_rowViewEnabled = doc["row_view"].get(); } + if (m_forceRowViewForCurrentTestList && m_runMode == RunMode::Batch) + m_rowViewEnabled = true; const auto baseDir = jsonPath.parent_path(); for (const auto& entry : doc["cases"]) @@ -846,5 +977,24 @@ bool MeshLoadersApp::reloadFromTestList() return startCase(0u); } +void MeshLoadersApp::resetRowViewScene() +{ + if (!isRowViewActive()) + return; + m_cases.clear(); + m_rowViewCache.clear(); + m_renderer->m_instances.clear(); + m_renderer->clearGeometries({ .semaphore = m_semaphore.get(),.value = m_realFrameIx }); +#ifdef NBL_BUILD_DEBUG_DRAW + m_aabbInstances.clear(); + m_obbInstances.clear(); +#endif + m_modelPath = "Row view (empty)"; + m_rowViewScreenshotCaptured = false; + m_shouldQuit = false; + m_nonInteractiveTest = false; + m_logger->log("Row view reset to empty. Press A to add a model.", ILogger::ELL_INFO); +} + diff --git a/12_MeshLoaders/MeshLoadersAppLoad.cpp b/12_MeshLoaders/MeshLoadersAppLoad.cpp index a91fc9050..aba231040 100644 --- a/12_MeshLoaders/MeshLoadersAppLoad.cpp +++ b/12_MeshLoaders/MeshLoadersAppLoad.cpp @@ -10,6 +10,65 @@ #include +namespace +{ +inline bool meshloadersIsFinite(const double value) +{ + return std::isfinite(value); +} + +inline bool meshloadersIsFinite(const hlsl::float64_t3& value) +{ + return meshloadersIsFinite(value.x) && meshloadersIsFinite(value.y) && meshloadersIsFinite(value.z); +} + +hlsl::shapes::AABB<3, double> meshloadersComputeFinitePositionAABB(const ICPUPolygonGeometry* geometry) +{ + auto aabb = hlsl::shapes::AABB<3, double>::create(); + if (!geometry) + return aabb; + const auto positionView = geometry->getPositionView(); + const auto vertexCount = positionView.getElementCount(); + bool hasFiniteVertex = false; + for (size_t i = 0u; i < vertexCount; ++i) + { + hlsl::float32_t3 decoded = {}; + positionView.decodeElement(i, decoded); + const hlsl::float64_t3 p = { + static_cast(decoded.x), + static_cast(decoded.y), + static_cast(decoded.z) + }; + if (!meshloadersIsFinite(p)) + continue; + if (!hasFiniteVertex) + { + aabb.minVx = p; + aabb.maxVx = p; + hasFiniteVertex = true; + continue; + } + aabb.minVx.x = std::min(aabb.minVx.x, p.x); + aabb.minVx.y = std::min(aabb.minVx.y, p.y); + aabb.minVx.z = std::min(aabb.minVx.z, p.z); + aabb.maxVx.x = std::max(aabb.maxVx.x, p.x); + aabb.maxVx.y = std::max(aabb.maxVx.y, p.y); + aabb.maxVx.z = std::max(aabb.maxVx.z, p.z); + } + if (hasFiniteVertex) + return aabb; + return hlsl::shapes::AABB<3, double>::create(); +} + +hlsl::shapes::AABB<3, double> meshloadersFallbackUnitAABB() +{ + hlsl::shapes::AABB<3, double> fallback = hlsl::shapes::AABB<3, double>::create(); + fallback.minVx = hlsl::float64_t3(-1.0, -1.0, -1.0); + fallback.maxVx = hlsl::float64_t3(1.0, 1.0, 1.0); + return fallback; +} +} + bool MeshLoadersApp::loadModel(const system::path& modelPath, bool updateCamera, bool storeCamera) { if (modelPath.empty()) @@ -151,7 +210,12 @@ bool MeshLoadersApp::loadModel(const system::path& modelPath, bool updateCamera, for (uint32_t i = 0; i < converted.size(); i++) { const auto& cpuGeom = geometries[i].get(); - const auto promoted = getGeometryAABB(cpuGeom); + auto promoted = getGeometryAABB(cpuGeom); + if (!isValidAABB(promoted)) + { + m_logger->log("Invalid geometry AABB for %s (geo=%u). Using fallback unit AABB for framing.", ILogger::ELL_WARNING, m_modelPath.c_str(), i); + promoted = meshloadersFallbackUnitAABB(); + } printAABB(promoted, "Geometry"); const auto promotedWorld = hlsl::float64_t3x4(worldTforms.emplace_back(hlsl::transpose(tmp))); const auto translation = hlsl::float64_t3( @@ -304,6 +368,12 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) const auto aabbStart = clock_t::now(); entry.aabb = getGeometryAABB(entry.cpu.get()); entry.hasAabb = isValidAABB(entry.aabb); + if (!entry.hasAabb) + { + m_logger->log("Invalid row-view geometry AABB for %s. Using fallback unit AABB.", ILogger::ELL_WARNING, path.string().c_str()); + entry.aabb = meshloadersFallbackUnitAABB(); + entry.hasAabb = true; + } stats.aabbMs += toMs(clock_t::now() - aabbStart); } else @@ -314,6 +384,12 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) const auto aabbStart = clock_t::now(); entry.aabb = getGeometryAABB(entry.cpu.get()); entry.hasAabb = isValidAABB(entry.aabb); + if (!entry.hasAabb) + { + m_logger->log("Invalid row-view geometry AABB for %s. Using fallback unit AABB.", ILogger::ELL_WARNING, path.string().c_str()); + entry.aabb = meshloadersFallbackUnitAABB(); + entry.hasAabb = true; + } stats.aabbMs += toMs(clock_t::now() - aabbStart); } } @@ -650,43 +726,38 @@ bool MeshLoadersApp::writeGeometry(smart_refctd_ptr g void MeshLoadersApp::setupCameraFromAABB(const hlsl::shapes::AABB<3, double>& bound) { - const auto extent = bound.getExtent(); + auto validBound = bound; + if (!isValidAABB(validBound)) + { + m_logger->log("Total AABB invalid; using fallback unit AABB for camera setup.", ILogger::ELL_WARNING); + validBound = meshloadersFallbackUnitAABB(); + } + const auto extent = validBound.getExtent(); const auto aspectRatio = double(m_window->getWidth()) / double(m_window->getHeight()); const double fovY = 1.2; const double fovX = 2.0 * std::atan(std::tan(fovY * 0.5) * aspectRatio); - const auto center = (bound.minVx + bound.maxVx) * 0.5; + const auto center = (validBound.minVx + validBound.maxVx) * 0.5; const auto halfExtent = extent * 0.5; const double halfX = std::max(halfExtent.x, 0.001); const double halfY = std::max(halfExtent.y, 0.001); const double halfZ = std::max(halfExtent.z, 0.001); const double safeRadius = std::max({ halfX, halfY, halfZ }); - struct CameraCandidate - { - hlsl::float64_t3 dir; - double planeHalfX; - double planeHalfY; - double depthHalf; - double footprint; - }; - std::array candidates = { - CameraCandidate{ hlsl::float64_t3(1.0, 0.0, 0.0), halfY, halfZ, halfX, halfY * halfZ }, - CameraCandidate{ hlsl::float64_t3(0.0, 1.0, 0.0), halfX, halfZ, halfY, halfX * halfZ }, - CameraCandidate{ hlsl::float64_t3(0.0, 0.0, 1.0), halfX, halfY, halfZ, halfX * halfY } - }; - const auto bestIt = std::max_element(candidates.begin(), candidates.end(), [](const CameraCandidate& a, const CameraCandidate& b) { return a.footprint < b.footprint; }); - const CameraCandidate best = (bestIt != candidates.end()) ? *bestIt : candidates[2u]; - - const double distY = best.planeHalfY / std::tan(fovY * 0.5); - const double distX = best.planeHalfX / std::tan(fovX * 0.5); + // Keep startup camera horizontal and in front of the scene. + const hlsl::float64_t3 dir(0.0, 0.0, 1.0); + const double planeHalfX = halfX; + const double planeHalfY = halfY; + const double depthHalf = halfZ; + const double distY = planeHalfY / std::tan(fovY * 0.5); + const double distX = planeHalfX / std::tan(fovX * 0.5); const double framingMargin = std::max(0.1, safeRadius * 0.35); - const double dist = std::max(distX, distY) + best.depthHalf + framingMargin; - - const auto dir = best.dir; - const auto pos = center + dir * dist; + const double dist = std::max(distX, distY) + depthHalf + framingMargin; + const double eyeHeightOffset = std::max(halfY * 0.2, 0.05); + const auto eyeCenter = center + hlsl::float64_t3(0.0, eyeHeightOffset, 0.0); + const auto pos = eyeCenter + dir * dist; - const double tightNear = std::max(0.0, dist - best.depthHalf - framingMargin); - const double tightFar = dist + best.depthHalf + framingMargin; + const double tightNear = std::max(0.0, dist - depthHalf - framingMargin); + const double tightFar = dist + depthHalf + framingMargin; const double nearByTight = tightNear * 0.01; const double nearByRadius = safeRadius * 0.002; const double nearPlane = std::max(0.001, std::min({ nearByTight, nearByRadius, 1.0 })); @@ -701,7 +772,7 @@ void MeshLoadersApp::setupCameraFromAABB(const hlsl::shapes::AABB<3, double>& bo const double moveSpeed = std::clamp(safeRadius * 0.015, 0.2, 40.0); camera.setMoveSpeed(static_cast(moveSpeed)); camera.setPosition(vectorSIMDf(pos.x, pos.y, pos.z)); - camera.setTarget(vectorSIMDf(center.x, center.y, center.z)); + camera.setTarget(vectorSIMDf(eyeCenter.x, eyeCenter.y, eyeCenter.z)); } hlsl::shapes::AABB<3, double> MeshLoadersApp::translateAABB(const hlsl::shapes::AABB<3, double>& aabb, const hlsl::float64_t3& translation) @@ -741,6 +812,8 @@ void MeshLoadersApp::applyCameraState(const CameraState& state) bool MeshLoadersApp::isValidAABB(const hlsl::shapes::AABB<3, double>& aabb) { return + meshloadersIsFinite(aabb.minVx) && + meshloadersIsFinite(aabb.maxVx) && (aabb.minVx.x <= aabb.maxVx.x) && (aabb.minVx.y <= aabb.maxVx.y) && (aabb.minVx.z <= aabb.maxVx.z); @@ -755,6 +828,8 @@ hlsl::shapes::AABB<3, double> MeshLoadersApp::getGeometryAABB(const ICPUPolygonG { CPolygonGeometryManipulator::recomputeAABB(geometry); aabb = geometry->getAABB>(); + if (!isValidAABB(aabb)) + aabb = meshloadersComputeFinitePositionAABB(geometry); } return aabb; } diff --git a/12_MeshLoaders/README.md b/12_MeshLoaders/README.md index dee05e92b..7577c42ce 100644 --- a/12_MeshLoaders/README.md +++ b/12_MeshLoaders/README.md @@ -29,6 +29,34 @@ Example for loading and writing `OBJ`, `PLY` and `STL` meshes. - Refresh geometry references: - run with `--update-references` (usually with `--ci`) +## Optional benchmark datasets via CMake +- Use this when you want larger/public inputs downloaded automatically. +- Public dataset repository: + - `https://github.com/Devsh-Graphics-Programming/Nabla-Benchmark-Datasets` +- Configure options: + - `NBL_MESHLOADERS_ENABLE_BENCHMARK_DATASETS=ON` + - `NBL_MESHLOADERS_DEFAULT_START_WITH_BENCHMARK_TESTLIST=ON|OFF` (default: `OFF`) + - `NBL_MESHLOADERS_BENCHMARK_DATASET_DIR=` (optional, default: build dir) + - `NBL_MESHLOADERS_BENCHMARK_DATASET_REPO=` (optional, default: public repo above) + - `NBL_MESHLOADERS_BENCHMARK_PAYLOAD_RELATIVE_PATH=` (optional, default: `inputs_benchmark.json`) +- What CMake does: + - fetches/clones dataset repo during configure via CMake `FetchContent` (if payload file is missing) + - resolves committed payload JSON from repo: + - `/` + - verifies payload is a regular Git file (not an LFS pointer) +- Run benchmark list with: + - `--testlist /` +- Default startup behavior when benchmark datasets are enabled: + - `NBL_MESHLOADERS_DEFAULT_START_WITH_BENCHMARK_TESTLIST=OFF`: still starts from local `inputs.json` (3 models) + - `NBL_MESHLOADERS_DEFAULT_START_WITH_BENCHMARK_TESTLIST=ON`: starts from benchmark payload test list +- Run benchmark CI directly via `ctest`: + - `ctest --output-on-failure -C Debug -R NBL_MESHLOADERS_CI_BENCHMARK` + - runs both benchmark CI modes: `heuristic` and `hybrid` + - benchmark CTest uses `--update-references` for payload-driven case names +- Run default CI directly via `ctest` (no benchmark datasets enabled): + - `ctest --output-on-failure -C Debug -R ^NBL_MESHLOADERS_CI$` + - uses default `inputs.json` (3 inputs) + ## CLI - `--ci` - strict validation run @@ -58,6 +86,7 @@ Example for loading and writing `OBJ`, `PLY` and `STL` meshes. - Left mouse drag: rotate camera - `Home`: reset view - `A`: add model to row view +- `X`: clear row view (empty scene) - `R`: reload row view from test list ## Input list format (`inputs.json`) From 55d1112550d6c0d717271a2b2dc663f87603481b Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Fri, 13 Feb 2026 17:22:30 +0100 Subject: [PATCH 10/34] Add optional hash parity CTests for meshloaders --- 12_MeshLoaders/CMakeLists.txt | 28 ++++ 12_MeshLoaders/MeshLoadersApp.hpp | 2 + 12_MeshLoaders/MeshLoadersAppLifecycle.cpp | 17 +++ 12_MeshLoaders/MeshLoadersAppLoad.cpp | 1 + 12_MeshLoaders/MeshLoadersAppRuntime.cpp | 159 +++++++++++++++++++++ 12_MeshLoaders/README.md | 12 ++ 6 files changed, 219 insertions(+) diff --git a/12_MeshLoaders/CMakeLists.txt b/12_MeshLoaders/CMakeLists.txt index 67c82bfa4..532683229 100644 --- a/12_MeshLoaders/CMakeLists.txt +++ b/12_MeshLoaders/CMakeLists.txt @@ -10,6 +10,7 @@ set(SRCs option(NBL_MESHLOADERS_ENABLE_BENCHMARK_DATASETS "Enable benchmark dataset clone + benchmark payload setup for 12_MeshLoaders." OFF) option(NBL_MESHLOADERS_DEFAULT_START_WITH_BENCHMARK_TESTLIST "When benchmark datasets are enabled, use benchmark payload as default startup test list in batch mode." OFF) +option(NBL_MESHLOADERS_ENABLE_HASH_CTESTS "Enable hash parity CTests (legacy_seq/new_seq/new_parallel) for 12_MeshLoaders." OFF) set(NBL_MESHLOADERS_BENCHMARK_DATASET_DIR "${CMAKE_BINARY_DIR}/meshloaders_benchmark_datasets" CACHE PATH "Destination directory for cloned 12_MeshLoaders benchmark datasets.") set(NBL_MESHLOADERS_BENCHMARK_DATASET_REPO "https://github.com/Devsh-Graphics-Programming/Nabla-Benchmark-Datasets.git" CACHE STRING "Git repository URL for 12_MeshLoaders benchmark datasets.") set(NBL_MESHLOADERS_BENCHMARK_PAYLOAD_RELATIVE_PATH "inputs_benchmark.json" CACHE STRING "Relative path to committed benchmark payload JSON inside dataset repo.") @@ -91,6 +92,33 @@ add_test(NAME NBL_MESHLOADERS_CI COMMAND_EXPAND_LISTS ) +if (NBL_MESHLOADERS_ENABLE_HASH_CTESTS) + set(_NBL_MESHLOADERS_HASH_TEST_COMMON_ARGS + --hash-test + ) + + if (NBL_MESHLOADERS_ENABLE_BENCHMARK_DATASETS) + list(APPEND _NBL_MESHLOADERS_HASH_TEST_COMMON_ARGS --testlist "${NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON}") + endif() + + add_test(NAME NBL_MESHLOADERS_HASH_TEST_HEURISTIC + COMMAND "$" ${_NBL_MESHLOADERS_HASH_TEST_COMMON_ARGS} --runtime-tuning heuristic + WORKING_DIRECTORY "$" + COMMAND_EXPAND_LISTS + ) + add_test(NAME NBL_MESHLOADERS_HASH_TEST_HYBRID + COMMAND "$" ${_NBL_MESHLOADERS_HASH_TEST_COMMON_ARGS} --runtime-tuning hybrid + WORKING_DIRECTORY "$" + COMMAND_EXPAND_LISTS + ) + set_tests_properties( + NBL_MESHLOADERS_HASH_TEST_HEURISTIC + NBL_MESHLOADERS_HASH_TEST_HYBRID + PROPERTIES + LABELS "meshloaders;hash;ci" + ) +endif() + if (NBL_MESHLOADERS_ENABLE_BENCHMARK_DATASETS) set(_NBL_MESHLOADERS_BENCHMARK_CI_COMMON_ARGS --ci diff --git a/12_MeshLoaders/MeshLoadersApp.hpp b/12_MeshLoaders/MeshLoadersApp.hpp index 7a57002e5..5119ecc02 100644 --- a/12_MeshLoaders/MeshLoadersApp.hpp +++ b/12_MeshLoaders/MeshLoadersApp.hpp @@ -147,6 +147,7 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc bool loadModel(const system::path& modelPath, bool updateCamera, bool storeCamera); bool loadRowView(RowViewReloadMode mode); bool writeGeometry(smart_refctd_ptr geometry, const std::string& savePath); + bool runHashConsistencyChecks(); void setupCameraFromAABB(const hlsl::shapes::AABB<3, double>& bound); static hlsl::shapes::AABB<3, double> translateAABB(const hlsl::shapes::AABB<3, double>& aabb, const hlsl::float64_t3& translation); @@ -230,6 +231,7 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc smart_refctd_ptr m_loaderPerfLogger; bool m_updateGeometryHashReferences = false; bool m_forceLoaderContentHashes = true; + bool m_hashTestOnly = false; asset::SFileIOPolicy::SRuntimeTuning::Mode m_runtimeTuningMode = asset::SFileIOPolicy::SRuntimeTuning::Mode::Heuristic; RunMode m_runMode = RunMode::Batch; diff --git a/12_MeshLoaders/MeshLoadersAppLifecycle.cpp b/12_MeshLoaders/MeshLoadersAppLifecycle.cpp index 82b24128e..ac25a5585 100644 --- a/12_MeshLoaders/MeshLoadersAppLifecycle.cpp +++ b/12_MeshLoaders/MeshLoadersAppLifecycle.cpp @@ -165,6 +165,9 @@ bool MeshLoadersApp::onAppInitialized(smart_refctd_ptr&& system) parser.add_argument("--ci") .help("Run in CI mode: load test list, write .ply, capture screenshots, compare data, and exit.") .flag(); + parser.add_argument("--hash-test") + .help("Run headless hash consistency check: parallel vs sequential content hash recompute, then exit.") + .flag(); parser.add_argument("--interactive") .help("Use file dialog to select a single model.") .flag(); @@ -205,6 +208,11 @@ bool MeshLoadersApp::onAppInitialized(smart_refctd_ptr&& system) m_runMode = RunMode::Interactive; if (parser["--ci"] == true) m_runMode = RunMode::CI; + if (parser["--hash-test"] == true) + { + m_hashTestOnly = true; + m_runMode = RunMode::CI; + } const bool hasExplicitTestListArg = parser.present("--testlist").has_value(); if (parser.present("--savepath")) @@ -348,6 +356,15 @@ bool MeshLoadersApp::onAppInitialized(smart_refctd_ptr&& system) if (!initTestCases()) return false; + if (m_hashTestOnly) + { + if (!runHashConsistencyChecks()) + return false; + m_shouldQuit = true; + onAppInitializedFinish(); + return true; + } + if (isRowViewActive()) { m_nonInteractiveTest = false; diff --git a/12_MeshLoaders/MeshLoadersAppLoad.cpp b/12_MeshLoaders/MeshLoadersAppLoad.cpp index aba231040..b5e9fb96d 100644 --- a/12_MeshLoaders/MeshLoadersAppLoad.cpp +++ b/12_MeshLoaders/MeshLoadersAppLoad.cpp @@ -108,6 +108,7 @@ bool MeshLoadersApp::loadModel(const system::path& modelPath, bool updateCamera, const auto extractMs = toMs(clock_t::now() - extractStart); if (geometries.empty()) failExit("No geometry found in asset %s.", m_modelPath.c_str()); + const auto outerMs = toMs(clock_t::now() - loadOuterStart); const auto nonLoaderMs = std::max(0.0, outerMs - loadMs); m_logger->log( diff --git a/12_MeshLoaders/MeshLoadersAppRuntime.cpp b/12_MeshLoaders/MeshLoadersAppRuntime.cpp index 260b73334..ac2f1f5b7 100644 --- a/12_MeshLoaders/MeshLoadersAppRuntime.cpp +++ b/12_MeshLoaders/MeshLoadersAppRuntime.cpp @@ -5,6 +5,26 @@ #include "MeshLoadersApp.hpp" #include "nbl/ext/ScreenShot/ScreenShot.h" +#include "nbl/asset/interchange/SGeometryContentHashCommon.h" +#include "nbl/core/hash/blake.h" + +namespace +{ + +core::blake3_hash_t meshloadersHashBufferLegacySequential(const asset::ICPUBuffer* const buffer) +{ + if (!buffer) + return static_cast(core::blake3_hasher{}); + const auto* const ptr = buffer->getPointer(); + const size_t size = buffer->getSize(); + if (!ptr || size == 0ull) + return static_cast(core::blake3_hasher{}); + core::blake3_hasher hasher; + hasher.update(ptr, size); + return static_cast(hasher); +} + +} std::string MeshLoadersApp::makeUniqueCaseName(const system::path& path) { @@ -86,6 +106,145 @@ core::blake3_hash_t MeshLoadersApp::hashGeometry(const ICPUPolygonGeometry* geo) return CPolygonGeometryManipulator::computeDeterministicContentHash(geo); } +bool MeshLoadersApp::runHashConsistencyChecks() +{ + using clock_t = std::chrono::high_resolution_clock; + + if (m_cases.empty()) + return logFail("Hash test requires at least one test case."); + + IAssetLoader::SAssetLoadParams params = makeLoadParams(); + params.logger = nullptr; + params.loaderFlags = static_cast(params.loaderFlags | IAssetLoader::ELPF_DONT_COMPUTE_CONTENT_HASHES); + + double totalLoadMs = 0.0; + double totalLegacySequentialMs = 0.0; + double totalNewSequentialMs = 0.0; + double totalNewParallelMs = 0.0; + uint64_t totalGeometryCount = 0ull; + uint64_t totalBufferCount = 0ull; + + for (const auto& testCase : m_cases) + { + m_assetMgr->clearAllAssetCache(); + + AssetLoadCallResult loadResult = {}; + if (!loadAssetCallFromPath(testCase.path, params, loadResult)) + failExit("Hash test failed to load input %s.", testCase.path.string().c_str()); + totalLoadMs += loadResult.getAssetMs; + + if (loadResult.bundle.getContents().empty()) + failExit("Hash test loaded empty asset for %s.", testCase.path.string().c_str()); + + core::vector> geometries; + if (!appendGeometriesFromBundle(loadResult.bundle, geometries)) + failExit("Hash test found no polygon geometry in %s.", testCase.path.string().c_str()); + + double caseLegacySequentialMs = 0.0; + double caseNewSequentialMs = 0.0; + double caseNewParallelMs = 0.0; + uint64_t caseBufferCount = 0ull; + + for (size_t geoIx = 0u; geoIx < geometries.size(); ++geoIx) + { + auto* geometry = const_cast(geometries[geoIx].get()); + if (!geometry) + failExit("Hash test failed to access geometry %llu in %s.", static_cast(geoIx), testCase.path.string().c_str()); + + core::vector> buffers; + asset::collectGeometryBuffers(geometry, buffers); + if (buffers.empty()) + continue; + + core::vector legacySequentialHashes; + core::vector newSequentialHashes; + core::vector newParallelHashes; + legacySequentialHashes.reserve(buffers.size()); + newSequentialHashes.reserve(buffers.size()); + newParallelHashes.reserve(buffers.size()); + + for (const auto& buffer : buffers) + { + if (!buffer) + continue; + + const auto* const ptr = buffer->getPointer(); + const size_t size = buffer->getSize(); + + const auto legacyStart = clock_t::now(); + const auto legacyHash = meshloadersHashBufferLegacySequential(buffer.get()); + caseLegacySequentialMs += toMs(clock_t::now() - legacyStart); + legacySequentialHashes.push_back(legacyHash); + + const auto newSeqStart = clock_t::now(); + const auto newSeqHash = core::blake3_hash_buffer_sequential(ptr, size); + caseNewSequentialMs += toMs(clock_t::now() - newSeqStart); + newSequentialHashes.push_back(newSeqHash); + + const auto newParStart = clock_t::now(); + const auto newParHash = core::blake3_hash_buffer(ptr, size); + caseNewParallelMs += toMs(clock_t::now() - newParStart); + newParallelHashes.push_back(newParHash); + } + + if (legacySequentialHashes.size() != newSequentialHashes.size() || legacySequentialHashes.size() != newParallelHashes.size()) + failExit("Hash test buffer count mismatch for %s geo=%llu.", testCase.path.string().c_str(), static_cast(geoIx)); + + for (size_t hashIx = 0u; hashIx < legacySequentialHashes.size(); ++hashIx) + { + if (legacySequentialHashes[hashIx] == newSequentialHashes[hashIx] && legacySequentialHashes[hashIx] == newParallelHashes[hashIx]) + continue; + failExit( + "Hash mismatch for %s geo=%llu buffer=%llu legacy_seq=%s new_seq=%s new_parallel=%s", + testCase.path.string().c_str(), + static_cast(geoIx), + static_cast(hashIx), + geometryHashToHex(legacySequentialHashes[hashIx]).c_str(), + geometryHashToHex(newSequentialHashes[hashIx]).c_str(), + geometryHashToHex(newParallelHashes[hashIx]).c_str()); + } + + caseBufferCount += legacySequentialHashes.size(); + ++totalGeometryCount; + } + + totalLegacySequentialMs += caseLegacySequentialMs; + totalNewSequentialMs += caseNewSequentialMs; + totalNewParallelMs += caseNewParallelMs; + totalBufferCount += caseBufferCount; + + if (m_logger) + { + m_logger->log( + "Hash test case: %s load=%.3f ms geos=%llu buffers=%llu legacy_seq=%.3f ms new_seq=%.3f ms new_parallel=%.3f ms", + ILogger::ELL_INFO, + testCase.path.string().c_str(), + loadResult.getAssetMs, + static_cast(geometries.size()), + static_cast(caseBufferCount), + caseLegacySequentialMs, + caseNewSequentialMs, + caseNewParallelMs); + } + } + + if (m_logger) + { + m_logger->log( + "Hash test summary: cases=%llu geos=%llu buffers=%llu load=%.3f ms legacy_seq=%.3f ms new_seq=%.3f ms new_parallel=%.3f ms", + ILogger::ELL_INFO, + static_cast(m_cases.size()), + static_cast(totalGeometryCount), + static_cast(totalBufferCount), + totalLoadMs, + totalLegacySequentialMs, + totalNewSequentialMs, + totalNewParallelMs); + } + + return true; +} + bool MeshLoadersApp::validateWrittenAsset(const system::path& path) { if (!std::filesystem::exists(path)) diff --git a/12_MeshLoaders/README.md b/12_MeshLoaders/README.md index 7577c42ce..eb6bfeecb 100644 --- a/12_MeshLoaders/README.md +++ b/12_MeshLoaders/README.md @@ -18,6 +18,8 @@ Example for loading and writing `OBJ`, `PLY` and `STL` meshes. - Opens file dialog and loads one model. - `ci` - Runs strict pass/fail validation per case. +- `hash-test` + - Headless hash parity check per geometry (`parallel` vs `sequential` recompute). ## Common workflows - Quick visual check: @@ -28,6 +30,8 @@ Example for loading and writing `OBJ`, `PLY` and `STL` meshes. - run with `--ci` - Refresh geometry references: - run with `--update-references` (usually with `--ci`) +- Validate content hash parity only (no write/screenshot roundtrip): + - run with `--hash-test` ## Optional benchmark datasets via CMake - Use this when you want larger/public inputs downloaded automatically. @@ -36,6 +40,7 @@ Example for loading and writing `OBJ`, `PLY` and `STL` meshes. - Configure options: - `NBL_MESHLOADERS_ENABLE_BENCHMARK_DATASETS=ON` - `NBL_MESHLOADERS_DEFAULT_START_WITH_BENCHMARK_TESTLIST=ON|OFF` (default: `OFF`) + - `NBL_MESHLOADERS_ENABLE_HASH_CTESTS=ON|OFF` (default: `OFF`) - `NBL_MESHLOADERS_BENCHMARK_DATASET_DIR=` (optional, default: build dir) - `NBL_MESHLOADERS_BENCHMARK_DATASET_REPO=` (optional, default: public repo above) - `NBL_MESHLOADERS_BENCHMARK_PAYLOAD_RELATIVE_PATH=` (optional, default: `inputs_benchmark.json`) @@ -56,12 +61,19 @@ Example for loading and writing `OBJ`, `PLY` and `STL` meshes. - Run default CI directly via `ctest` (no benchmark datasets enabled): - `ctest --output-on-failure -C Debug -R ^NBL_MESHLOADERS_CI$` - uses default `inputs.json` (3 inputs) +- Run hash parity tests directly via `ctest`: + - `ctest --output-on-failure -C Debug -R NBL_MESHLOADERS_HASH_TEST` + - requires `NBL_MESHLOADERS_ENABLE_HASH_CTESTS=ON` at configure time + - runs both tuning modes: `heuristic` and `hybrid` + - if benchmark datasets are enabled, hash tests use benchmark payload test list ## CLI - `--ci` - strict validation run - `--interactive` - file-dialog run +- `--hash-test` + - headless load + hash parity check (`parallel` vs `sequential`), then exit - `--testlist ` - custom JSON list - `--savegeometry` From b39dea98e8e5b01eacca35765f5f1e6ed08eee9d Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Fri, 13 Feb 2026 19:08:59 +0100 Subject: [PATCH 11/34] Scope transfer-src swapchain usage to meshloaders --- 12_MeshLoaders/MeshLoadersApp.hpp | 1 + 12_MeshLoaders/MeshLoadersAppLifecycle.cpp | 5 +++++ common/include/nbl/examples/common/MonoWindowApplication.hpp | 4 +++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/12_MeshLoaders/MeshLoadersApp.hpp b/12_MeshLoaders/MeshLoadersApp.hpp index 5119ecc02..2e815213e 100644 --- a/12_MeshLoaders/MeshLoadersApp.hpp +++ b/12_MeshLoaders/MeshLoadersApp.hpp @@ -114,6 +114,7 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc return system::ILogger::DefaultLogMask() | system::ILogger::ELL_INFO; } + void configureSwapchainCreationParams(video::ISwapchain::SCreationParams& params) const override; const video::IGPURenderpass::SCreationParams::SSubpassDependency* getDefaultSubpassDependencies() const override; private: diff --git a/12_MeshLoaders/MeshLoadersAppLifecycle.cpp b/12_MeshLoaders/MeshLoadersAppLifecycle.cpp index ac25a5585..30fd23a44 100644 --- a/12_MeshLoaders/MeshLoadersAppLifecycle.cpp +++ b/12_MeshLoaders/MeshLoadersAppLifecycle.cpp @@ -124,6 +124,11 @@ MeshLoadersApp::MeshLoadersApp( { } +void MeshLoadersApp::configureSwapchainCreationParams(video::ISwapchain::SCreationParams& params) const +{ + params.sharedParams.imageUsage |= IGPUImage::E_USAGE_FLAGS::EUF_TRANSFER_SRC_BIT; +} + bool MeshLoadersApp::onAppInitialized(smart_refctd_ptr&& system) { if (!asset_base_t::onAppInitialized(smart_refctd_ptr(system))) diff --git a/common/include/nbl/examples/common/MonoWindowApplication.hpp b/common/include/nbl/examples/common/MonoWindowApplication.hpp index 658b4146b..0454d3671 100644 --- a/common/include/nbl/examples/common/MonoWindowApplication.hpp +++ b/common/include/nbl/examples/common/MonoWindowApplication.hpp @@ -70,7 +70,7 @@ class MonoWindowApplication : public virtual SimpleWindowedApplication return false; ISwapchain::SCreationParams swapchainParams = { .surface = smart_refctd_ptr(m_surface->getSurface()) }; - swapchainParams.sharedParams.imageUsage |= IGPUImage::E_USAGE_FLAGS::EUF_TRANSFER_SRC_BIT; + configureSwapchainCreationParams(swapchainParams); if (!swapchainParams.deduceFormat(m_physicalDevice)) return logFail("Could not choose a Surface Format for the Swapchain!"); @@ -159,6 +159,8 @@ class MonoWindowApplication : public virtual SimpleWindowedApplication } protected: + virtual inline void configureSwapchainCreationParams(video::ISwapchain::SCreationParams&) const {} + inline void onAppInitializedFinish() { m_winMgr->show(m_window.get()); From 294a21a2a661566c6548b0ce7bb93c05edd885a6 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 14 Feb 2026 07:34:28 +0100 Subject: [PATCH 12/34] Keep transfer-src swapchain setup local to meshloaders --- 12_MeshLoaders/MeshLoadersApp.hpp | 149 +++++++++++++++++- 12_MeshLoaders/MeshLoadersAppLifecycle.cpp | 5 - .../examples/common/MonoWindowApplication.hpp | 3 - 3 files changed, 146 insertions(+), 11 deletions(-) diff --git a/12_MeshLoaders/MeshLoadersApp.hpp b/12_MeshLoaders/MeshLoadersApp.hpp index 2e815213e..f563a1706 100644 --- a/12_MeshLoaders/MeshLoadersApp.hpp +++ b/12_MeshLoaders/MeshLoadersApp.hpp @@ -6,6 +6,9 @@ // For conditions of distribution and use, see copyright notice in nabla.h #include "common.hpp" +#include "nbl/examples/common/SimpleWindowedApplication.hpp" +#include "nbl/examples/common/CSwapchainFramebuffersAndDepth.hpp" +#include "nbl/examples/common/CEventCallback.hpp" #include #include @@ -18,9 +21,150 @@ #include "nbl/ext/DebugDraw/CDrawAABB.h" #endif -class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourcesApplication +class MeshLoadersWindowedApplication : public virtual SimpleWindowedApplication { - using device_base_t = MonoWindowApplication; + using base_t = SimpleWindowedApplication; + +public: + constexpr static inline uint8_t MaxFramesInFlight = 3; + + template + MeshLoadersWindowedApplication(const hlsl::uint16_t2 initialResolution, const asset::E_FORMAT depthFormat, Args&&... args) + : base_t(std::forward(args)...), m_initialResolution(initialResolution), m_depthFormat(depthFormat) {} + + core::vector getSurfaces() const override final + { + if (!m_surface) + { + auto windowCallback = make_smart_refctd_ptr(smart_refctd_ptr(m_inputSystem), smart_refctd_ptr(m_logger)); + IWindow::SCreationParams params = {}; + params.callback = make_smart_refctd_ptr(); + params.width = m_initialResolution[0]; + params.height = m_initialResolution[1]; + params.x = 32; + params.y = 32; + params.flags = ui::IWindow::ECF_HIDDEN | IWindow::ECF_BORDERLESS | IWindow::ECF_RESIZABLE | IWindow::ECF_CAN_MINIMIZE; + params.windowCaption = "MeshLoaders"; + params.callback = windowCallback; + const_cast&>(m_window) = m_winMgr->createWindow(std::move(params)); + + auto surface = CSurfaceVulkanWin32::create(smart_refctd_ptr(m_api), smart_refctd_ptr_static_cast(m_window)); + const_cast&>(m_surface) = CSimpleResizeSurface::create(std::move(surface)); + } + + if (m_surface) + return { {m_surface->getSurface()} }; + + return {}; + } + + bool onAppInitialized(core::smart_refctd_ptr&& system) override + { + if (!MonoSystemMonoLoggerApplication::onAppInitialized(std::move(system))) + return false; + + m_inputSystem = make_smart_refctd_ptr(system::logger_opt_smart_ptr(smart_refctd_ptr(m_logger))); + if (!base_t::onAppInitialized(std::move(system))) + return false; + + ISwapchain::SCreationParams swapchainParams = { .surface = smart_refctd_ptr(m_surface->getSurface()) }; + swapchainParams.sharedParams.imageUsage |= IGPUImage::E_USAGE_FLAGS::EUF_TRANSFER_SRC_BIT; + if (!swapchainParams.deduceFormat(m_physicalDevice)) + return logFail("Could not choose a Surface Format for the Swapchain!"); + + auto scResources = std::make_unique(m_device.get(), m_depthFormat, swapchainParams.surfaceFormat.format, getDefaultSubpassDependencies()); + auto* renderpass = scResources->getRenderpass(); + if (!renderpass) + return logFail("Failed to create Renderpass!"); + + auto gQueue = getGraphicsQueue(); + if (!m_surface || !m_surface->init(gQueue, std::move(scResources), swapchainParams.sharedParams)) + return logFail("Could not create Window & Surface or initialize the Surface!"); + + m_winMgr->setWindowSize(m_window.get(), m_initialResolution[0], m_initialResolution[1]); + m_surface->recreateSwapchain(); + return true; + } + + void workLoopBody() override final + { + const uint32_t framesInFlightCount = hlsl::min(MaxFramesInFlight, m_surface->getMaxAcquiresInFlight()); + if (m_framesInFlight.size() >= framesInFlightCount) + { + const ISemaphore::SWaitInfo framesDone[] = { {.semaphore = m_framesInFlight.front().semaphore.get(), .value = m_framesInFlight.front().value} }; + if (m_device->blockForSemaphores(framesDone) != ISemaphore::WAIT_RESULT::SUCCESS) + return; + m_framesInFlight.pop_front(); + } + + auto updatePresentationTimestamp = [&]() + { + m_currentImageAcquire = m_surface->acquireNextImage(); + oracle.reportEndFrameRecord(); + const auto timestamp = oracle.getNextPresentationTimeStamp(); + oracle.reportBeginFrameRecord(); + return timestamp; + }; + + const auto nextPresentationTimestamp = updatePresentationTimestamp(); + if (!m_currentImageAcquire) + return; + + const IQueue::SSubmitInfo::SSemaphoreInfo rendered[] = { renderFrame(nextPresentationTimestamp) }; + m_surface->present(m_currentImageAcquire.imageIndex, rendered); + if (rendered->semaphore) + m_framesInFlight.emplace_back(smart_refctd_ptr(rendered->semaphore), rendered->value); + } + + bool keepRunning() override + { + if (m_surface->irrecoverable()) + return false; + return true; + } + + bool onAppTerminated() override + { + m_inputSystem = nullptr; + m_device->waitIdle(); + m_framesInFlight.clear(); + m_surface = nullptr; + m_window = nullptr; + return base_t::onAppTerminated(); + } + +protected: + inline void onAppInitializedFinish() + { + m_winMgr->show(m_window.get()); + oracle.reportBeginFrameRecord(); + } + + inline const auto& getCurrentAcquire() const { return m_currentImageAcquire; } + + virtual const video::IGPURenderpass::SCreationParams::SSubpassDependency* getDefaultSubpassDependencies() const = 0; + virtual video::IQueue::SSubmitInfo::SSemaphoreInfo renderFrame(const std::chrono::microseconds nextPresentationTimestamp) = 0; + + const hlsl::uint16_t2 m_initialResolution; + const asset::E_FORMAT m_depthFormat; + core::smart_refctd_ptr m_inputSystem; + core::smart_refctd_ptr m_window; + core::smart_refctd_ptr> m_surface; + +private: + struct SSubmittedFrame + { + core::smart_refctd_ptr semaphore; + uint64_t value; + }; + core::deque m_framesInFlight; + video::ISimpleManagedSurface::SAcquireResult m_currentImageAcquire = {}; + video::CDumbPresentationOracle oracle; +}; + +class MeshLoadersApp final : public MeshLoadersWindowedApplication, public BuiltinResourcesApplication +{ + using device_base_t = MeshLoadersWindowedApplication; using asset_base_t = BuiltinResourcesApplication; enum DrawBoundingBoxMode @@ -114,7 +258,6 @@ class MeshLoadersApp final : public MonoWindowApplication, public BuiltinResourc return system::ILogger::DefaultLogMask() | system::ILogger::ELL_INFO; } - void configureSwapchainCreationParams(video::ISwapchain::SCreationParams& params) const override; const video::IGPURenderpass::SCreationParams::SSubpassDependency* getDefaultSubpassDependencies() const override; private: diff --git a/12_MeshLoaders/MeshLoadersAppLifecycle.cpp b/12_MeshLoaders/MeshLoadersAppLifecycle.cpp index 30fd23a44..ac25a5585 100644 --- a/12_MeshLoaders/MeshLoadersAppLifecycle.cpp +++ b/12_MeshLoaders/MeshLoadersAppLifecycle.cpp @@ -124,11 +124,6 @@ MeshLoadersApp::MeshLoadersApp( { } -void MeshLoadersApp::configureSwapchainCreationParams(video::ISwapchain::SCreationParams& params) const -{ - params.sharedParams.imageUsage |= IGPUImage::E_USAGE_FLAGS::EUF_TRANSFER_SRC_BIT; -} - bool MeshLoadersApp::onAppInitialized(smart_refctd_ptr&& system) { if (!asset_base_t::onAppInitialized(smart_refctd_ptr(system))) diff --git a/common/include/nbl/examples/common/MonoWindowApplication.hpp b/common/include/nbl/examples/common/MonoWindowApplication.hpp index 0454d3671..7fd322e34 100644 --- a/common/include/nbl/examples/common/MonoWindowApplication.hpp +++ b/common/include/nbl/examples/common/MonoWindowApplication.hpp @@ -70,7 +70,6 @@ class MonoWindowApplication : public virtual SimpleWindowedApplication return false; ISwapchain::SCreationParams swapchainParams = { .surface = smart_refctd_ptr(m_surface->getSurface()) }; - configureSwapchainCreationParams(swapchainParams); if (!swapchainParams.deduceFormat(m_physicalDevice)) return logFail("Could not choose a Surface Format for the Swapchain!"); @@ -159,8 +158,6 @@ class MonoWindowApplication : public virtual SimpleWindowedApplication } protected: - virtual inline void configureSwapchainCreationParams(video::ISwapchain::SCreationParams&) const {} - inline void onAppInitializedFinish() { m_winMgr->show(m_window.get()); From d8227dbab6ceafa3462c21041134070c27e97bfa Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Thu, 19 Feb 2026 13:58:43 +0100 Subject: [PATCH 13/34] Address PR 250 review comments --- 12_MeshLoaders/CMakeLists.txt | 41 ++-- 12_MeshLoaders/MeshLoadersApp.hpp | 154 ++----------- 12_MeshLoaders/MeshLoadersAppLifecycle.cpp | 104 +++++---- 12_MeshLoaders/MeshLoadersAppLoad.cpp | 116 ++-------- 12_MeshLoaders/MeshLoadersAppRuntime.cpp | 218 ++++++++---------- 12_MeshLoaders/README.md | 13 +- .../bin/references/Spanner-ply.geomhash | 2 +- .../bin/references/Stanford_Bunny.geomhash | 2 +- .../bin/references/yellowflower.geomhash | 2 +- .../examples/common/GeometryAABBUtilities.h | 145 ++++++++++++ .../nbl/examples/common/ImageComparison.h | 63 +++++ .../examples/common/MonoWindowApplication.hpp | 16 +- 12 files changed, 440 insertions(+), 436 deletions(-) create mode 100644 common/include/nbl/examples/common/GeometryAABBUtilities.h create mode 100644 common/include/nbl/examples/common/ImageComparison.h diff --git a/12_MeshLoaders/CMakeLists.txt b/12_MeshLoaders/CMakeLists.txt index 532683229..42d992cd9 100644 --- a/12_MeshLoaders/CMakeLists.txt +++ b/12_MeshLoaders/CMakeLists.txt @@ -14,6 +14,7 @@ option(NBL_MESHLOADERS_ENABLE_HASH_CTESTS "Enable hash parity CTests (legacy_seq set(NBL_MESHLOADERS_BENCHMARK_DATASET_DIR "${CMAKE_BINARY_DIR}/meshloaders_benchmark_datasets" CACHE PATH "Destination directory for cloned 12_MeshLoaders benchmark datasets.") set(NBL_MESHLOADERS_BENCHMARK_DATASET_REPO "https://github.com/Devsh-Graphics-Programming/Nabla-Benchmark-Datasets.git" CACHE STRING "Git repository URL for 12_MeshLoaders benchmark datasets.") set(NBL_MESHLOADERS_BENCHMARK_PAYLOAD_RELATIVE_PATH "inputs_benchmark.json" CACHE STRING "Relative path to committed benchmark payload JSON inside dataset repo.") +set(NBL_MESHLOADERS_MEDIA_PAYLOAD_RELATIVE_PATH "inputs_benchmark.json" CACHE STRING "Preferred benchmark payload relative path inside examples_tests/media.") set(NBL_INCLUDE_SEARCH_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}/include" @@ -46,23 +47,29 @@ add_dependencies(${EXECUTABLE_NAME} nlohmann_json::nlohmann_json) target_include_directories(${EXECUTABLE_NAME} PUBLIC $) if (NBL_MESHLOADERS_ENABLE_BENCHMARK_DATASETS) - set(NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON "${NBL_MESHLOADERS_BENCHMARK_DATASET_DIR}/${NBL_MESHLOADERS_BENCHMARK_PAYLOAD_RELATIVE_PATH}" CACHE FILEPATH "Committed benchmark testlist for 12_MeshLoaders." FORCE) - if (NOT EXISTS "${NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON}") - if (EXISTS "${NBL_MESHLOADERS_BENCHMARK_DATASET_DIR}" AND NOT EXISTS "${NBL_MESHLOADERS_BENCHMARK_DATASET_DIR}/.git") - message(STATUS "[meshloaders-bench] Dataset dir exists without .git, skipping fetch and using local files: ${NBL_MESHLOADERS_BENCHMARK_DATASET_DIR}") - else() - include(FetchContent) - FetchContent_Declare(nbl_meshloaders_benchmark_dataset - GIT_REPOSITORY "${NBL_MESHLOADERS_BENCHMARK_DATASET_REPO}" - GIT_TAG "master" - GIT_SHALLOW TRUE - GIT_PROGRESS TRUE - SOURCE_DIR "${NBL_MESHLOADERS_BENCHMARK_DATASET_DIR}" - BINARY_DIR "${CMAKE_BINARY_DIR}/CMakeFiles/nbl_meshloaders_benchmark_dataset-build" - ) - FetchContent_GetProperties(nbl_meshloaders_benchmark_dataset) - if (NOT nbl_meshloaders_benchmark_dataset_POPULATED) - FetchContent_Populate(nbl_meshloaders_benchmark_dataset) + set(NBL_MESHLOADERS_MEDIA_INPUTS_JSON "${CMAKE_CURRENT_SOURCE_DIR}/../media/${NBL_MESHLOADERS_MEDIA_PAYLOAD_RELATIVE_PATH}") + if (EXISTS "${NBL_MESHLOADERS_MEDIA_INPUTS_JSON}") + set(NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON "${NBL_MESHLOADERS_MEDIA_INPUTS_JSON}" CACHE FILEPATH "Committed benchmark testlist for 12_MeshLoaders." FORCE) + message(STATUS "[meshloaders-bench] Using examples media payload: ${NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON}") + else() + set(NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON "${NBL_MESHLOADERS_BENCHMARK_DATASET_DIR}/${NBL_MESHLOADERS_BENCHMARK_PAYLOAD_RELATIVE_PATH}" CACHE FILEPATH "Committed benchmark testlist for 12_MeshLoaders." FORCE) + if (NOT EXISTS "${NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON}") + if (EXISTS "${NBL_MESHLOADERS_BENCHMARK_DATASET_DIR}" AND NOT EXISTS "${NBL_MESHLOADERS_BENCHMARK_DATASET_DIR}/.git") + message(STATUS "[meshloaders-bench] Dataset dir exists without .git, skipping fetch and using local files: ${NBL_MESHLOADERS_BENCHMARK_DATASET_DIR}") + else() + include(FetchContent) + FetchContent_Declare(nbl_meshloaders_benchmark_dataset + GIT_REPOSITORY "${NBL_MESHLOADERS_BENCHMARK_DATASET_REPO}" + GIT_TAG "master" + GIT_SHALLOW TRUE + GIT_PROGRESS TRUE + SOURCE_DIR "${NBL_MESHLOADERS_BENCHMARK_DATASET_DIR}" + BINARY_DIR "${CMAKE_BINARY_DIR}/CMakeFiles/nbl_meshloaders_benchmark_dataset-build" + ) + FetchContent_GetProperties(nbl_meshloaders_benchmark_dataset) + if (NOT nbl_meshloaders_benchmark_dataset_POPULATED) + FetchContent_Populate(nbl_meshloaders_benchmark_dataset) + endif () endif () endif () endif () diff --git a/12_MeshLoaders/MeshLoadersApp.hpp b/12_MeshLoaders/MeshLoadersApp.hpp index f563a1706..e9a08b202 100644 --- a/12_MeshLoaders/MeshLoadersApp.hpp +++ b/12_MeshLoaders/MeshLoadersApp.hpp @@ -6,9 +6,7 @@ // For conditions of distribution and use, see copyright notice in nabla.h #include "common.hpp" -#include "nbl/examples/common/SimpleWindowedApplication.hpp" -#include "nbl/examples/common/CSwapchainFramebuffersAndDepth.hpp" -#include "nbl/examples/common/CEventCallback.hpp" +#include "nbl/examples/common/MonoWindowApplication.hpp" #include #include @@ -21,145 +19,24 @@ #include "nbl/ext/DebugDraw/CDrawAABB.h" #endif -class MeshLoadersWindowedApplication : public virtual SimpleWindowedApplication +class MeshLoadersWindowedApplication : public virtual nbl::examples::MonoWindowApplication { - using base_t = SimpleWindowedApplication; + using base_t = nbl::examples::MonoWindowApplication; public: - constexpr static inline uint8_t MaxFramesInFlight = 3; - template MeshLoadersWindowedApplication(const hlsl::uint16_t2 initialResolution, const asset::E_FORMAT depthFormat, Args&&... args) - : base_t(std::forward(args)...), m_initialResolution(initialResolution), m_depthFormat(depthFormat) {} + : base_t(initialResolution, depthFormat, std::forward(args)...) {} - core::vector getSurfaces() const override final +protected: + inline const char* getWindowCaption() const override { - if (!m_surface) - { - auto windowCallback = make_smart_refctd_ptr(smart_refctd_ptr(m_inputSystem), smart_refctd_ptr(m_logger)); - IWindow::SCreationParams params = {}; - params.callback = make_smart_refctd_ptr(); - params.width = m_initialResolution[0]; - params.height = m_initialResolution[1]; - params.x = 32; - params.y = 32; - params.flags = ui::IWindow::ECF_HIDDEN | IWindow::ECF_BORDERLESS | IWindow::ECF_RESIZABLE | IWindow::ECF_CAN_MINIMIZE; - params.windowCaption = "MeshLoaders"; - params.callback = windowCallback; - const_cast&>(m_window) = m_winMgr->createWindow(std::move(params)); - - auto surface = CSurfaceVulkanWin32::create(smart_refctd_ptr(m_api), smart_refctd_ptr_static_cast(m_window)); - const_cast&>(m_surface) = CSimpleResizeSurface::create(std::move(surface)); - } - - if (m_surface) - return { {m_surface->getSurface()} }; - - return {}; + return "MeshLoaders"; } - - bool onAppInitialized(core::smart_refctd_ptr&& system) override + inline void amendSwapchainCreateParams(video::ISwapchain::SCreationParams& swapchainParams) const override { - if (!MonoSystemMonoLoggerApplication::onAppInitialized(std::move(system))) - return false; - - m_inputSystem = make_smart_refctd_ptr(system::logger_opt_smart_ptr(smart_refctd_ptr(m_logger))); - if (!base_t::onAppInitialized(std::move(system))) - return false; - - ISwapchain::SCreationParams swapchainParams = { .surface = smart_refctd_ptr(m_surface->getSurface()) }; swapchainParams.sharedParams.imageUsage |= IGPUImage::E_USAGE_FLAGS::EUF_TRANSFER_SRC_BIT; - if (!swapchainParams.deduceFormat(m_physicalDevice)) - return logFail("Could not choose a Surface Format for the Swapchain!"); - - auto scResources = std::make_unique(m_device.get(), m_depthFormat, swapchainParams.surfaceFormat.format, getDefaultSubpassDependencies()); - auto* renderpass = scResources->getRenderpass(); - if (!renderpass) - return logFail("Failed to create Renderpass!"); - - auto gQueue = getGraphicsQueue(); - if (!m_surface || !m_surface->init(gQueue, std::move(scResources), swapchainParams.sharedParams)) - return logFail("Could not create Window & Surface or initialize the Surface!"); - - m_winMgr->setWindowSize(m_window.get(), m_initialResolution[0], m_initialResolution[1]); - m_surface->recreateSwapchain(); - return true; - } - - void workLoopBody() override final - { - const uint32_t framesInFlightCount = hlsl::min(MaxFramesInFlight, m_surface->getMaxAcquiresInFlight()); - if (m_framesInFlight.size() >= framesInFlightCount) - { - const ISemaphore::SWaitInfo framesDone[] = { {.semaphore = m_framesInFlight.front().semaphore.get(), .value = m_framesInFlight.front().value} }; - if (m_device->blockForSemaphores(framesDone) != ISemaphore::WAIT_RESULT::SUCCESS) - return; - m_framesInFlight.pop_front(); - } - - auto updatePresentationTimestamp = [&]() - { - m_currentImageAcquire = m_surface->acquireNextImage(); - oracle.reportEndFrameRecord(); - const auto timestamp = oracle.getNextPresentationTimeStamp(); - oracle.reportBeginFrameRecord(); - return timestamp; - }; - - const auto nextPresentationTimestamp = updatePresentationTimestamp(); - if (!m_currentImageAcquire) - return; - - const IQueue::SSubmitInfo::SSemaphoreInfo rendered[] = { renderFrame(nextPresentationTimestamp) }; - m_surface->present(m_currentImageAcquire.imageIndex, rendered); - if (rendered->semaphore) - m_framesInFlight.emplace_back(smart_refctd_ptr(rendered->semaphore), rendered->value); - } - - bool keepRunning() override - { - if (m_surface->irrecoverable()) - return false; - return true; } - - bool onAppTerminated() override - { - m_inputSystem = nullptr; - m_device->waitIdle(); - m_framesInFlight.clear(); - m_surface = nullptr; - m_window = nullptr; - return base_t::onAppTerminated(); - } - -protected: - inline void onAppInitializedFinish() - { - m_winMgr->show(m_window.get()); - oracle.reportBeginFrameRecord(); - } - - inline const auto& getCurrentAcquire() const { return m_currentImageAcquire; } - - virtual const video::IGPURenderpass::SCreationParams::SSubpassDependency* getDefaultSubpassDependencies() const = 0; - virtual video::IQueue::SSubmitInfo::SSemaphoreInfo renderFrame(const std::chrono::microseconds nextPresentationTimestamp) = 0; - - const hlsl::uint16_t2 m_initialResolution; - const asset::E_FORMAT m_depthFormat; - core::smart_refctd_ptr m_inputSystem; - core::smart_refctd_ptr m_window; - core::smart_refctd_ptr> m_surface; - -private: - struct SSubmittedFrame - { - core::smart_refctd_ptr semaphore; - uint64_t value; - }; - core::deque m_framesInFlight; - video::ISimpleManagedSurface::SAcquireResult m_currentImageAcquire = {}; - video::CDumbPresentationOracle oracle; }; class MeshLoadersApp final : public MeshLoadersWindowedApplication, public BuiltinResourcesApplication @@ -231,8 +108,8 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built struct CameraState { - core::vectorSIMDf position; - core::vectorSIMDf target; + hlsl::float32_t3 position = hlsl::float32_t3(0.0f, 0.0f, 0.0f); + hlsl::float32_t3 target = hlsl::float32_t3(0.0f, 0.0f, -1.0f); nbl::hlsl::float32_t4x4 projection; float moveSpeed = 1.0f; }; @@ -250,7 +127,6 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built bool onAppInitialized(smart_refctd_ptr&& system) override; IQueue::SSubmitInfo::SSemaphoreInfo renderFrame(const std::chrono::microseconds nextPresentationTimestamp) override; bool onAppTerminated() override; - bool keepRunning() override; protected: core::bitflag getLogLevelMask() override @@ -271,6 +147,9 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built static std::string normalizeExtension(const system::path& path); bool isWriteExtensionSupported(const std::string& ext) const; system::path resolveSavePath(const system::path& modelPath) const; + static system::path resolveRuntimeCWD(const system::path& preferred); + bool parseCommandLineOptions(const system::path& effectiveInputCWD, const system::path& effectiveOutputCWD, const system::path& defaultBenchmarkTestListPath); + std::string makeCaptionModelPath() const; static std::string sanitizeCaseNameForFilename(std::string name); system::path getGeometryHashReferencePath(const std::string& caseName) const; @@ -294,8 +173,6 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built bool runHashConsistencyChecks(); void setupCameraFromAABB(const hlsl::shapes::AABB<3, double>& bound); - static hlsl::shapes::AABB<3, double> translateAABB(const hlsl::shapes::AABB<3, double>& aabb, const hlsl::float64_t3& translation); - static hlsl::shapes::AABB<3, double> scaleAABB(const hlsl::shapes::AABB<3, double>& aabb, double scale); void storeCameraState(); void applyCameraState(const CameraState& state); @@ -320,16 +197,17 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built bool validateWrittenAsset(const system::path& path); bool captureScreenshot(const system::path& path, core::smart_refctd_ptr& outImage); bool appendGeometriesFromBundle(const asset::SAssetBundle& bundle, core::vector>& out) const; - bool compareImages(const asset::ICPUImageView* a, const asset::ICPUImageView* b, uint64_t& diffCount, uint8_t& maxDiff); + bool compareImages(const asset::ICPUImageView* a, const asset::ICPUImageView* b, uint64_t& diffCount, uint16_t& maxDiff); void advanceCase(); + bool shouldKeepRunning() const override; constexpr static inline uint32_t MaxFramesInFlight = 3u; constexpr static inline uint32_t CiFramesBeforeCapture = 10u; constexpr static inline uint32_t NonCiFramesPerCase = 120u; constexpr static inline uint32_t RowViewFramesBeforeCapture = 10u; constexpr static inline uint64_t MaxImageDiffBytes = 16u; - constexpr static inline uint8_t MaxImageDiffValue = 1u; + constexpr static inline uint16_t MaxImageDiffValue = 1u; smart_refctd_ptr m_renderer; smart_refctd_ptr m_semaphore; diff --git a/12_MeshLoaders/MeshLoadersAppLifecycle.cpp b/12_MeshLoaders/MeshLoadersAppLifecycle.cpp index ac25a5585..3c533cbd8 100644 --- a/12_MeshLoaders/MeshLoadersAppLifecycle.cpp +++ b/12_MeshLoaders/MeshLoadersAppLifecycle.cpp @@ -21,12 +21,16 @@ #include "nbl/system/CFileLogger.h" -namespace +system::path MeshLoadersApp::resolveRuntimeCWD(const system::path& preferred) { + if (preferred.empty() || preferred == path("/") || preferred == path("\\")) + return path(std::filesystem::current_path()); + return preferred; +} -template -std::string makeCaptionModelPath(const std::string& modelPath, const ArgContainer& argv) +std::string MeshLoadersApp::makeCaptionModelPath() const { + const auto& modelPath = m_modelPath; if (modelPath.empty()) return {}; @@ -112,47 +116,24 @@ std::string makeCaptionModelPath(const std::string& modelPath, const ArgContaine return targetPath.generic_string(); } -} - MeshLoadersApp::MeshLoadersApp( const path& localInputCWD, const path& localOutputCWD, const path& sharedInputCWD, const path& sharedOutputCWD) - : IApplicationFramework(localInputCWD, localOutputCWD, sharedInputCWD, sharedOutputCWD) + : nbl::examples::MonoWindowApplication({1280, 720}, EF_D32_SFLOAT, localInputCWD, localOutputCWD, sharedInputCWD, sharedOutputCWD) + , IApplicationFramework(localInputCWD, localOutputCWD, sharedInputCWD, sharedOutputCWD) , device_base_t({1280, 720}, EF_D32_SFLOAT, localInputCWD, localOutputCWD, sharedInputCWD, sharedOutputCWD) { } -bool MeshLoadersApp::onAppInitialized(smart_refctd_ptr&& system) +bool MeshLoadersApp::parseCommandLineOptions(const system::path& effectiveInputCWD, const system::path& effectiveOutputCWD, const system::path& defaultBenchmarkTestListPath) { - if (!asset_base_t::onAppInitialized(smart_refctd_ptr(system))) - return false; -#ifdef NBL_BUILD_MITSUBA_LOADER - m_assetMgr->addAssetLoader(make_smart_refctd_ptr()); -#endif - if (!device_base_t::onAppInitialized(smart_refctd_ptr(system))) - return false; - - const auto resolveRuntimeCWD = [](const path& preferred)->path - { - if (preferred.empty() || preferred == path("/") || preferred == path("\\")) - return path(std::filesystem::current_path()); - return preferred; - }; - const path effectiveInputCWD = resolveRuntimeCWD(localInputCWD); - const path effectiveOutputCWD = resolveRuntimeCWD(localOutputCWD); - m_runMode = RunMode::Batch; m_saveGeomPrefixPath = effectiveOutputCWD / "saved"; m_screenshotPrefixPath = effectiveOutputCWD / "screenshots"; m_testListPath = effectiveInputCWD / "inputs.json"; m_forceRowViewForCurrentTestList = false; -#if defined(NBL_MESHLOADERS_DEFAULT_BENCHMARK_TESTLIST_PATH) - const path defaultBenchmarkTestListPath = path(NBL_MESHLOADERS_DEFAULT_BENCHMARK_TESTLIST_PATH); -#else - const path defaultBenchmarkTestListPath; -#endif argparse::ArgumentParser parser("12_meshloaders"); parser.add_argument("--savegeometry") @@ -166,7 +147,7 @@ bool MeshLoadersApp::onAppInitialized(smart_refctd_ptr&& system) .help("Run in CI mode: load test list, write .ply, capture screenshots, compare data, and exit.") .flag(); parser.add_argument("--hash-test") - .help("Run headless hash consistency check: parallel vs sequential content hash recompute, then exit.") + .help("Run headless prehash consistency check: invalid before recompute, valid after recompute, then exit.") .flag(); parser.add_argument("--interactive") .help("Use file dialog to select a single model.") @@ -218,13 +199,10 @@ bool MeshLoadersApp::onAppInitialized(smart_refctd_ptr&& system) if (parser.present("--savepath")) { auto tmp = path(parser.get("--savepath")); - if (tmp.empty() || !tmp.has_filename()) return logFail("Invalid path has been specified in --savepath argument"); - if (!std::filesystem::exists(tmp.parent_path())) return logFail("Path specified in --savepath argument doesn't exist"); - m_specifiedGeomSavePath.emplace(std::move(tmp.generic_string())); } @@ -247,6 +225,7 @@ bool MeshLoadersApp::onAppInitialized(smart_refctd_ptr&& system) m_logger->log("Using benchmark test list for default batch startup: %s", ILogger::ELL_INFO, m_testListPath.string().c_str()); } } + if (parser.present("--row-add")) { auto tmp = path(parser.get("--row-add")); @@ -293,6 +272,29 @@ bool MeshLoadersApp::onAppInitialized(smart_refctd_ptr&& system) return logFail("Invalid --runtime-tuning value. Expected: none|heuristic|hybrid."); } + return true; +} + +bool MeshLoadersApp::onAppInitialized(smart_refctd_ptr&& system) +{ + if (!asset_base_t::onAppInitialized(smart_refctd_ptr(system))) + return false; +#ifdef NBL_BUILD_MITSUBA_LOADER + m_assetMgr->addAssetLoader(make_smart_refctd_ptr()); +#endif + if (!device_base_t::onAppInitialized(smart_refctd_ptr(system))) + return false; + + const path effectiveInputCWD = resolveRuntimeCWD(localInputCWD); + const path effectiveOutputCWD = resolveRuntimeCWD(localOutputCWD); +#if defined(NBL_MESHLOADERS_DEFAULT_BENCHMARK_TESTLIST_PATH) + const path defaultBenchmarkTestListPath = path(NBL_MESHLOADERS_DEFAULT_BENCHMARK_TESTLIST_PATH); +#else + const path defaultBenchmarkTestListPath; +#endif + if (!parseCommandLineOptions(effectiveInputCWD, effectiveOutputCWD, defaultBenchmarkTestListPath)) + return false; + const path inputReferencesDir = effectiveInputCWD / "references"; const path outputReferencesDir = effectiveOutputCWD / "references"; std::error_code referenceDirEc; @@ -340,6 +342,7 @@ bool MeshLoadersApp::onAppInitialized(smart_refctd_ptr&& system) return logFail("Failed to create renderer!"); #ifdef NBL_BUILD_DEBUG_DRAW + if (!m_hashTestOnly) { auto* renderpass = scRes->getRenderpass(); ext::debug_draw::DrawAABB::SCreationParameters params = {}; @@ -440,10 +443,13 @@ IQueue::SSubmitInfo::SSemaphoreInfo MeshLoadersApp::renderFrame(const std::chron // late latch input if (!m_nonInteractiveTest) { - bool reloadInteractiveRequested = false; - bool reloadListRequested = false; - bool addRowViewRequested = false; - bool clearRowViewRequested = false; + struct SPendingInputActions + { + bool reloadInteractive = false; + bool reloadList = false; + bool addRowView = false; + bool clearRowView = false; + } pending; camera.beginInputProcessing(nextPresentationTimestamp); mouse.consumeEvents([&](const IMouseEventChannel::range_t& events) -> void { camera.mouseProcess(events); }, m_logger.get()); keyboard.consumeEvents([&](const IKeyboardEventChannel::range_t& events) -> void @@ -455,19 +461,19 @@ IQueue::SSubmitInfo::SSemaphoreInfo MeshLoadersApp::renderFrame(const std::chron if (event.keyCode == E_KEY_CODE::EKC_R) { if (isRowViewActive()) - reloadListRequested = true; + pending.reloadList = true; else - reloadInteractiveRequested = true; + pending.reloadInteractive = true; } else if (event.keyCode == E_KEY_CODE::EKC_A) { if (isRowViewActive()) - addRowViewRequested = true; + pending.addRowView = true; } else if (event.keyCode == E_KEY_CODE::EKC_X) { if (isRowViewActive()) - clearRowViewRequested = true; + pending.clearRowView = true; } } camera.keyboardProcess(events); @@ -475,16 +481,16 @@ IQueue::SSubmitInfo::SSemaphoreInfo MeshLoadersApp::renderFrame(const std::chron m_logger.get() ); camera.endInputProcessing(nextPresentationTimestamp); - if (clearRowViewRequested) + if (pending.clearRowView) resetRowViewScene(); - if (addRowViewRequested) + if (pending.addRowView) addRowViewCase(); - if (reloadListRequested) + if (pending.reloadList) { if (!reloadFromTestList()) failExit("Failed to reload test list."); } - if (reloadInteractiveRequested) + if (pending.reloadInteractive) reloadInteractive(); } // draw scene @@ -541,7 +547,7 @@ IQueue::SSubmitInfo::SSemaphoreInfo MeshLoadersApp::renderFrame(const std::chron std::string caption = "[Nabla Engine] Mesh Loaders"; { caption += ", displaying ["; - caption += makeCaptionModelPath(m_modelPath, argv); + caption += makeCaptionModelPath(); caption += "]"; m_window->setCaption(caption); } @@ -560,11 +566,9 @@ bool MeshLoadersApp::onAppTerminated() return device_base_t::onAppTerminated(); } -bool MeshLoadersApp::keepRunning() +bool MeshLoadersApp::shouldKeepRunning() const { - if (m_shouldQuit) - return false; - return device_base_t::keepRunning(); + return !m_shouldQuit; } const video::IGPURenderpass::SCreationParams::SSubpassDependency* MeshLoadersApp::getDefaultSubpassDependencies() const diff --git a/12_MeshLoaders/MeshLoadersAppLoad.cpp b/12_MeshLoaders/MeshLoadersAppLoad.cpp index b5e9fb96d..88f5a9ee6 100644 --- a/12_MeshLoaders/MeshLoadersAppLoad.cpp +++ b/12_MeshLoaders/MeshLoadersAppLoad.cpp @@ -9,65 +9,8 @@ #include #include - -namespace -{ -inline bool meshloadersIsFinite(const double value) -{ - return std::isfinite(value); -} - -inline bool meshloadersIsFinite(const hlsl::float64_t3& value) -{ - return meshloadersIsFinite(value.x) && meshloadersIsFinite(value.y) && meshloadersIsFinite(value.z); -} - -hlsl::shapes::AABB<3, double> meshloadersComputeFinitePositionAABB(const ICPUPolygonGeometry* geometry) -{ - auto aabb = hlsl::shapes::AABB<3, double>::create(); - if (!geometry) - return aabb; - const auto positionView = geometry->getPositionView(); - const auto vertexCount = positionView.getElementCount(); - bool hasFiniteVertex = false; - for (size_t i = 0u; i < vertexCount; ++i) - { - hlsl::float32_t3 decoded = {}; - positionView.decodeElement(i, decoded); - const hlsl::float64_t3 p = { - static_cast(decoded.x), - static_cast(decoded.y), - static_cast(decoded.z) - }; - if (!meshloadersIsFinite(p)) - continue; - if (!hasFiniteVertex) - { - aabb.minVx = p; - aabb.maxVx = p; - hasFiniteVertex = true; - continue; - } - aabb.minVx.x = std::min(aabb.minVx.x, p.x); - aabb.minVx.y = std::min(aabb.minVx.y, p.y); - aabb.minVx.z = std::min(aabb.minVx.z, p.z); - aabb.maxVx.x = std::max(aabb.maxVx.x, p.x); - aabb.maxVx.y = std::max(aabb.maxVx.y, p.y); - aabb.maxVx.z = std::max(aabb.maxVx.z, p.z); - } - if (hasFiniteVertex) - return aabb; - return hlsl::shapes::AABB<3, double>::create(); -} - -hlsl::shapes::AABB<3, double> meshloadersFallbackUnitAABB() -{ - hlsl::shapes::AABB<3, double> fallback = hlsl::shapes::AABB<3, double>::create(); - fallback.minVx = hlsl::float64_t3(-1.0, -1.0, -1.0); - fallback.maxVx = hlsl::float64_t3(1.0, 1.0, 1.0); - return fallback; -} -} +#include +#include "nbl/examples/common/GeometryAABBUtilities.h" bool MeshLoadersApp::loadModel(const system::path& modelPath, bool updateCamera, bool storeCamera) { @@ -215,15 +158,11 @@ bool MeshLoadersApp::loadModel(const system::path& modelPath, bool updateCamera, if (!isValidAABB(promoted)) { m_logger->log("Invalid geometry AABB for %s (geo=%u). Using fallback unit AABB for framing.", ILogger::ELL_WARNING, m_modelPath.c_str(), i); - promoted = meshloadersFallbackUnitAABB(); + promoted = nbl::examples::geometry::fallbackUnitAABB(); } printAABB(promoted, "Geometry"); const auto promotedWorld = hlsl::float64_t3x4(worldTforms.emplace_back(hlsl::transpose(tmp))); - const auto translation = hlsl::float64_t3( - static_cast(tmp[3].x), - static_cast(tmp[3].y), - static_cast(tmp[3].z)); - const auto transformed = translateAABB(promoted, translation); + const auto transformed = nbl::hlsl::math::linalg::pseudo_mul(promotedWorld, promoted); printAABB(transformed, "Transformed"); bound = hlsl::shapes::util::union_(transformed, bound); @@ -372,7 +311,7 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) if (!entry.hasAabb) { m_logger->log("Invalid row-view geometry AABB for %s. Using fallback unit AABB.", ILogger::ELL_WARNING, path.string().c_str()); - entry.aabb = meshloadersFallbackUnitAABB(); + entry.aabb = nbl::examples::geometry::fallbackUnitAABB(); entry.hasAabb = true; } stats.aabbMs += toMs(clock_t::now() - aabbStart); @@ -388,7 +327,7 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) if (!entry.hasAabb) { m_logger->log("Invalid row-view geometry AABB for %s. Using fallback unit AABB.", ILogger::ELL_WARNING, path.string().c_str()); - entry.aabb = meshloadersFallbackUnitAABB(); + entry.aabb = nbl::examples::geometry::fallbackUnitAABB(); entry.hasAabb = true; } stats.aabbMs += toMs(clock_t::now() - aabbStart); @@ -612,9 +551,7 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) tmp[3] = hlsl::float32_t3(static_cast(tx), static_cast(ty), static_cast(tz)); const auto promotedWorld = hlsl::float64_t3x4(worldTforms.emplace_back(hlsl::transpose(tmp))); - const auto translation = hlsl::float64_t3(tx, ty, tz); - const auto scaled = scaleAABB(aabb, scale); - const auto transformed = translateAABB(scaled, translation); + const auto transformed = nbl::hlsl::math::linalg::pseudo_mul(promotedWorld, aabb); printAABB(transformed, "Transformed"); bound = hlsl::shapes::util::union_(transformed, bound); @@ -731,7 +668,7 @@ void MeshLoadersApp::setupCameraFromAABB(const hlsl::shapes::AABB<3, double>& bo if (!isValidAABB(validBound)) { m_logger->log("Total AABB invalid; using fallback unit AABB for camera setup.", ILogger::ELL_WARNING); - validBound = meshloadersFallbackUnitAABB(); + validBound = nbl::examples::geometry::fallbackUnitAABB(); } const auto extent = validBound.getExtent(); const auto aspectRatio = double(m_window->getWidth()) / double(m_window->getHeight()); @@ -764,7 +701,7 @@ void MeshLoadersApp::setupCameraFromAABB(const hlsl::shapes::AABB<3, double>& bo const double nearPlane = std::max(0.001, std::min({ nearByTight, nearByRadius, 1.0 })); const double farPlane = std::max({ tightFar * 16.0, nearPlane + safeRadius * 24.0 + 10.0, dist + safeRadius * 24.0 }); - const auto projection = nbl::hlsl::buildProjectionMatrixPerspectiveFovRH( + const auto projection = nbl::hlsl::math::thin_lens::rhPerspectiveFovMatrix( static_cast(fovY), static_cast(aspectRatio), static_cast(nearPlane), @@ -776,27 +713,13 @@ void MeshLoadersApp::setupCameraFromAABB(const hlsl::shapes::AABB<3, double>& bo camera.setTarget(vectorSIMDf(eyeCenter.x, eyeCenter.y, eyeCenter.z)); } -hlsl::shapes::AABB<3, double> MeshLoadersApp::translateAABB(const hlsl::shapes::AABB<3, double>& aabb, const hlsl::float64_t3& translation) -{ - auto out = aabb; - out.minVx += translation; - out.maxVx += translation; - return out; -} - -hlsl::shapes::AABB<3, double> MeshLoadersApp::scaleAABB(const hlsl::shapes::AABB<3, double>& aabb, const double scale) -{ - auto out = aabb; - out.minVx *= scale; - out.maxVx *= scale; - return out; -} - void MeshLoadersApp::storeCameraState() { + const auto position = camera.getPosition(); + const auto target = camera.getTarget(); m_referenceCamera = CameraState{ - camera.getPosition(), - camera.getTarget(), + hlsl::float32_t3(position.x, position.y, position.z), + hlsl::float32_t3(target.x, target.y, target.z), camera.getProjectionMatrix(), camera.getMoveSpeed() }; @@ -805,19 +728,14 @@ void MeshLoadersApp::storeCameraState() void MeshLoadersApp::applyCameraState(const CameraState& state) { camera.setProjectionMatrix(state.projection); - camera.setPosition(state.position); - camera.setTarget(state.target); + camera.setPosition(vectorSIMDf(state.position.x, state.position.y, state.position.z)); + camera.setTarget(vectorSIMDf(state.target.x, state.target.y, state.target.z)); camera.setMoveSpeed(state.moveSpeed); } bool MeshLoadersApp::isValidAABB(const hlsl::shapes::AABB<3, double>& aabb) { - return - meshloadersIsFinite(aabb.minVx) && - meshloadersIsFinite(aabb.maxVx) && - (aabb.minVx.x <= aabb.maxVx.x) && - (aabb.minVx.y <= aabb.maxVx.y) && - (aabb.minVx.z <= aabb.maxVx.z); + return nbl::examples::geometry::isValidAABB(aabb); } hlsl::shapes::AABB<3, double> MeshLoadersApp::getGeometryAABB(const ICPUPolygonGeometry* geometry) const @@ -830,7 +748,7 @@ hlsl::shapes::AABB<3, double> MeshLoadersApp::getGeometryAABB(const ICPUPolygonG CPolygonGeometryManipulator::recomputeAABB(geometry); aabb = geometry->getAABB>(); if (!isValidAABB(aabb)) - aabb = meshloadersComputeFinitePositionAABB(geometry); + aabb = nbl::examples::geometry::computeFiniteUsedPositionAABB(geometry); } return aabb; } diff --git a/12_MeshLoaders/MeshLoadersAppRuntime.cpp b/12_MeshLoaders/MeshLoadersAppRuntime.cpp index ac2f1f5b7..a121db615 100644 --- a/12_MeshLoaders/MeshLoadersAppRuntime.cpp +++ b/12_MeshLoaders/MeshLoadersAppRuntime.cpp @@ -4,27 +4,11 @@ #include "MeshLoadersApp.hpp" +#include "nbl/asset/IPreHashed.h" #include "nbl/ext/ScreenShot/ScreenShot.h" #include "nbl/asset/interchange/SGeometryContentHashCommon.h" #include "nbl/core/hash/blake.h" - -namespace -{ - -core::blake3_hash_t meshloadersHashBufferLegacySequential(const asset::ICPUBuffer* const buffer) -{ - if (!buffer) - return static_cast(core::blake3_hasher{}); - const auto* const ptr = buffer->getPointer(); - const size_t size = buffer->getSize(); - if (!ptr || size == 0ull) - return static_cast(core::blake3_hasher{}); - core::blake3_hasher hasher; - hasher.update(ptr, size); - return static_cast(hasher); -} - -} +#include "nbl/examples/common/ImageComparison.h" std::string MeshLoadersApp::makeUniqueCaseName(const system::path& path) { @@ -103,7 +87,31 @@ void MeshLoadersApp::logRowViewLoadTotal(const double ms, const size_t hits, con core::blake3_hash_t MeshLoadersApp::hashGeometry(const ICPUPolygonGeometry* geo) { - return CPolygonGeometryManipulator::computeDeterministicContentHash(geo); + if (!geo) + return asset::IPreHashed::INVALID_HASH; + + auto* mutableGeo = const_cast(geo); + CPolygonGeometryManipulator::recomputeContentHashes(mutableGeo); + + core::vector> buffers; + asset::SPolygonGeometryContentHash::collectBuffers(mutableGeo, buffers); + if (buffers.empty()) + return asset::IPreHashed::INVALID_HASH; + + core::blake3_hasher hasher; + if (const auto* indexing = geo->getIndexingCallback(); indexing) + { + hasher << indexing->degree(); + hasher << indexing->rate(); + hasher << indexing->knownTopology(); + } + for (const auto& buffer : buffers) + { + if (!buffer) + continue; + hasher << buffer->getContentHash(); + } + return static_cast(hasher); } bool MeshLoadersApp::runHashConsistencyChecks() @@ -118,11 +126,9 @@ bool MeshLoadersApp::runHashConsistencyChecks() params.loaderFlags = static_cast(params.loaderFlags | IAssetLoader::ELPF_DONT_COMPUTE_CONTENT_HASHES); double totalLoadMs = 0.0; - double totalLegacySequentialMs = 0.0; - double totalNewSequentialMs = 0.0; - double totalNewParallelMs = 0.0; uint64_t totalGeometryCount = 0ull; uint64_t totalBufferCount = 0ull; + uint64_t totalInvalidBefore = 0ull; for (const auto& testCase : m_cases) { @@ -140,10 +146,8 @@ bool MeshLoadersApp::runHashConsistencyChecks() if (!appendGeometriesFromBundle(loadResult.bundle, geometries)) failExit("Hash test found no polygon geometry in %s.", testCase.path.string().c_str()); - double caseLegacySequentialMs = 0.0; - double caseNewSequentialMs = 0.0; - double caseNewParallelMs = 0.0; uint64_t caseBufferCount = 0ull; + uint64_t caseInvalidBefore = 0ull; for (size_t geoIx = 0u; geoIx < geometries.size(); ++geoIx) { @@ -152,94 +156,105 @@ bool MeshLoadersApp::runHashConsistencyChecks() failExit("Hash test failed to access geometry %llu in %s.", static_cast(geoIx), testCase.path.string().c_str()); core::vector> buffers; - asset::collectGeometryBuffers(geometry, buffers); + asset::SPolygonGeometryContentHash::collectBuffers(geometry, buffers); if (buffers.empty()) continue; - core::vector legacySequentialHashes; - core::vector newSequentialHashes; - core::vector newParallelHashes; - legacySequentialHashes.reserve(buffers.size()); - newSequentialHashes.reserve(buffers.size()); - newParallelHashes.reserve(buffers.size()); - for (const auto& buffer : buffers) { if (!buffer) continue; + if (buffer->getContentHash() != asset::IPreHashed::INVALID_HASH) + failExit("Hash test expected invalid prehash for %s geo=%llu.", testCase.path.string().c_str(), static_cast(geoIx)); + ++caseInvalidBefore; + } - const auto* const ptr = buffer->getPointer(); - const size_t size = buffer->getSize(); + const auto recomputeStart = clock_t::now(); + const auto aggregateHash = hashGeometry(geometry); + const auto recomputeMs = toMs(clock_t::now() - recomputeStart); + if (aggregateHash == asset::IPreHashed::INVALID_HASH) + failExit("Hash test recompute failed for %s geo=%llu.", testCase.path.string().c_str(), static_cast(geoIx)); - const auto legacyStart = clock_t::now(); - const auto legacyHash = meshloadersHashBufferLegacySequential(buffer.get()); - caseLegacySequentialMs += toMs(clock_t::now() - legacyStart); - legacySequentialHashes.push_back(legacyHash); + for (size_t bufferIx = 0u; bufferIx < buffers.size(); ++bufferIx) + { + const auto& buffer = buffers[bufferIx]; + if (!buffer) + continue; - const auto newSeqStart = clock_t::now(); - const auto newSeqHash = core::blake3_hash_buffer_sequential(ptr, size); - caseNewSequentialMs += toMs(clock_t::now() - newSeqStart); - newSequentialHashes.push_back(newSeqHash); + if (buffer->getContentHash() == asset::IPreHashed::INVALID_HASH) + failExit("Hash test buffer still invalid for %s geo=%llu buffer=%llu.", testCase.path.string().c_str(), static_cast(geoIx), static_cast(bufferIx)); - const auto newParStart = clock_t::now(); - const auto newParHash = core::blake3_hash_buffer(ptr, size); - caseNewParallelMs += toMs(clock_t::now() - newParStart); - newParallelHashes.push_back(newParHash); - } + const auto* const ptr = buffer->getPointer(); + const size_t size = buffer->getSize(); + if (!ptr || size == 0ull) + continue; - if (legacySequentialHashes.size() != newSequentialHashes.size() || legacySequentialHashes.size() != newParallelHashes.size()) - failExit("Hash test buffer count mismatch for %s geo=%llu.", testCase.path.string().c_str(), static_cast(geoIx)); + const auto expected = core::blake3_hash_buffer(ptr, size); + if (expected != buffer->getContentHash()) + { + failExit( + "Hash test mismatch for %s geo=%llu buffer=%llu expected=%s actual=%s", + testCase.path.string().c_str(), + static_cast(geoIx), + static_cast(bufferIx), + geometryHashToHex(expected).c_str(), + geometryHashToHex(buffer->getContentHash()).c_str()); + } + } - for (size_t hashIx = 0u; hashIx < legacySequentialHashes.size(); ++hashIx) + const auto aggregateHashRepeat = hashGeometry(geometry); + if (aggregateHashRepeat != aggregateHash) { - if (legacySequentialHashes[hashIx] == newSequentialHashes[hashIx] && legacySequentialHashes[hashIx] == newParallelHashes[hashIx]) - continue; failExit( - "Hash mismatch for %s geo=%llu buffer=%llu legacy_seq=%s new_seq=%s new_parallel=%s", + "Hash test aggregate instability for %s geo=%llu first=%s second=%s", + testCase.path.string().c_str(), + static_cast(geoIx), + geometryHashToHex(aggregateHash).c_str(), + geometryHashToHex(aggregateHashRepeat).c_str()); + } + + if (m_logger) + { + m_logger->log( + "Hash test geometry: %s geo=%llu buffers=%llu recompute=%.3f ms aggregate=%s", + ILogger::ELL_INFO, testCase.path.string().c_str(), static_cast(geoIx), - static_cast(hashIx), - geometryHashToHex(legacySequentialHashes[hashIx]).c_str(), - geometryHashToHex(newSequentialHashes[hashIx]).c_str(), - geometryHashToHex(newParallelHashes[hashIx]).c_str()); + static_cast(buffers.size()), + recomputeMs, + geometryHashToHex(aggregateHash).c_str()); } - caseBufferCount += legacySequentialHashes.size(); + caseBufferCount += buffers.size(); ++totalGeometryCount; } - totalLegacySequentialMs += caseLegacySequentialMs; - totalNewSequentialMs += caseNewSequentialMs; - totalNewParallelMs += caseNewParallelMs; totalBufferCount += caseBufferCount; + totalInvalidBefore += caseInvalidBefore; if (m_logger) { m_logger->log( - "Hash test case: %s load=%.3f ms geos=%llu buffers=%llu legacy_seq=%.3f ms new_seq=%.3f ms new_parallel=%.3f ms", + "Hash test case: %s load=%.3f ms geos=%llu buffers=%llu invalid_before=%llu", ILogger::ELL_INFO, testCase.path.string().c_str(), loadResult.getAssetMs, static_cast(geometries.size()), static_cast(caseBufferCount), - caseLegacySequentialMs, - caseNewSequentialMs, - caseNewParallelMs); + static_cast(caseInvalidBefore)); } } if (m_logger) { m_logger->log( - "Hash test summary: cases=%llu geos=%llu buffers=%llu load=%.3f ms legacy_seq=%.3f ms new_seq=%.3f ms new_parallel=%.3f ms", + "Hash test summary: cases=%llu geos=%llu buffers=%llu invalid_before=%llu load=%.3f ms", ILogger::ELL_INFO, static_cast(m_cases.size()), static_cast(totalGeometryCount), static_cast(totalBufferCount), - totalLoadMs, - totalLegacySequentialMs, - totalNewSequentialMs, - totalNewParallelMs); + static_cast(totalInvalidBefore), + totalLoadMs); } return true; @@ -325,19 +340,18 @@ bool MeshLoadersApp::appendGeometriesFromBundle(const asset::SAssetBundle& bundl case IAsset::E_TYPE::ET_GEOMETRY_COLLECTION: for (const auto& item : bundle.getContents()) { - auto collection = IAsset::castDown(item); + auto collection = IAsset::castDown(item); if (!collection) continue; - auto* refs = collection->getGeometries(); - if (!refs) - continue; - for (const auto& ref : *refs) + const auto& refs = collection->getGeometries(); + for (const auto& ref : refs) { if (!ref.geometry) continue; if (ref.geometry->getPrimitiveType() != IGeometryBase::EPrimitiveType::Polygon) continue; - auto poly = core::smart_refctd_ptr_static_cast(ref.geometry); + const auto assetRef = core::smart_refctd_ptr_static_cast(ref.geometry); + auto poly = IAsset::castDown(assetRef); if (poly) out.push_back(poly); } @@ -350,53 +364,9 @@ bool MeshLoadersApp::appendGeometriesFromBundle(const asset::SAssetBundle& bundl return !out.empty(); } -bool MeshLoadersApp::compareImages(const asset::ICPUImageView* a, const asset::ICPUImageView* b, uint64_t& diffCount, uint8_t& maxDiff) +bool MeshLoadersApp::compareImages(const asset::ICPUImageView* a, const asset::ICPUImageView* b, uint64_t& diffCount, uint16_t& maxDiff) { - diffCount = 0u; - maxDiff = 0u; - if (!a || !b) - return false; - - const auto* imgA = a->getCreationParameters().image.get(); - const auto* imgB = b->getCreationParameters().image.get(); - if (!imgA || !imgB) - return false; - - const auto paramsA = imgA->getCreationParameters(); - const auto paramsB = imgB->getCreationParameters(); - if (paramsA.format != paramsB.format) - return false; - if (paramsA.extent != paramsB.extent) - return false; - - const auto* bufA = imgA->getBuffer(); - const auto* bufB = imgB->getBuffer(); - if (!bufA || !bufB) - return false; - - const size_t sizeA = bufA->getSize(); - if (sizeA != bufB->getSize()) - return false; - - const auto* dataA = static_cast(bufA->getPointer()); - const auto* dataB = static_cast(bufB->getPointer()); - if (!dataA || !dataB) - return false; - - for (size_t i = 0; i < sizeA; ++i) - { - const uint8_t va = dataA[i]; - const uint8_t vb = dataB[i]; - const uint8_t diff = va > vb ? static_cast(va - vb) : static_cast(vb - va); - if (diff) - { - ++diffCount; - if (diff > maxDiff) - maxDiff = diff; - } - } - - return true; + return nbl::examples::image::compareCpuImageViewsByCodeUnit(a, b, diffCount, maxDiff); } void MeshLoadersApp::advanceCase() @@ -458,7 +428,7 @@ void MeshLoadersApp::advanceCase() } uint64_t diffCount = 0u; - uint8_t maxDiff = 0u; + uint16_t maxDiff = 0u; if (!compareImages(m_loadedScreenshot.get(), m_writtenScreenshot.get(), diffCount, maxDiff)) failExit("Image compare failed for %s.", m_caseName.c_str()); if (diffCount > MaxImageDiffBytes || maxDiff > MaxImageDiffValue) diff --git a/12_MeshLoaders/README.md b/12_MeshLoaders/README.md index eb6bfeecb..fafccf661 100644 --- a/12_MeshLoaders/README.md +++ b/12_MeshLoaders/README.md @@ -19,7 +19,11 @@ Example for loading and writing `OBJ`, `PLY` and `STL` meshes. - `ci` - Runs strict pass/fail validation per case. - `hash-test` - - Headless hash parity check per geometry (`parallel` vs `sequential` recompute). + - Headless `IPreHashed` integrity check per geometry (invalid before recompute, valid after recompute). + +## Row view concept +- `row_view` means one scene containing all cases from the test list. +- Each case is normalized and laid out left-to-right so camera framing is stable for comparisons. ## Common workflows - Quick visual check: @@ -30,7 +34,7 @@ Example for loading and writing `OBJ`, `PLY` and `STL` meshes. - run with `--ci` - Refresh geometry references: - run with `--update-references` (usually with `--ci`) -- Validate content hash parity only (no write/screenshot roundtrip): +- Validate content hash consistency only (no write/screenshot roundtrip): - run with `--hash-test` ## Optional benchmark datasets via CMake @@ -45,6 +49,7 @@ Example for loading and writing `OBJ`, `PLY` and `STL` meshes. - `NBL_MESHLOADERS_BENCHMARK_DATASET_REPO=` (optional, default: public repo above) - `NBL_MESHLOADERS_BENCHMARK_PAYLOAD_RELATIVE_PATH=` (optional, default: `inputs_benchmark.json`) - What CMake does: + - first tries payload from `examples_tests/media/` - fetches/clones dataset repo during configure via CMake `FetchContent` (if payload file is missing) - resolves committed payload JSON from repo: - `/` @@ -61,7 +66,7 @@ Example for loading and writing `OBJ`, `PLY` and `STL` meshes. - Run default CI directly via `ctest` (no benchmark datasets enabled): - `ctest --output-on-failure -C Debug -R ^NBL_MESHLOADERS_CI$` - uses default `inputs.json` (3 inputs) -- Run hash parity tests directly via `ctest`: +- Run hash consistency tests directly via `ctest`: - `ctest --output-on-failure -C Debug -R NBL_MESHLOADERS_HASH_TEST` - requires `NBL_MESHLOADERS_ENABLE_HASH_CTESTS=ON` at configure time - runs both tuning modes: `heuristic` and `hybrid` @@ -73,7 +78,7 @@ Example for loading and writing `OBJ`, `PLY` and `STL` meshes. - `--interactive` - file-dialog run - `--hash-test` - - headless load + hash parity check (`parallel` vs `sequential`), then exit + - headless load + prehash consistency check, then exit - `--testlist ` - custom JSON list - `--savegeometry` diff --git a/12_MeshLoaders/bin/references/Spanner-ply.geomhash b/12_MeshLoaders/bin/references/Spanner-ply.geomhash index 1fc5eb419..05abdb30a 100644 --- a/12_MeshLoaders/bin/references/Spanner-ply.geomhash +++ b/12_MeshLoaders/bin/references/Spanner-ply.geomhash @@ -1 +1 @@ -264ac1d6b5de49770560164959e9f2dcc25ceb9c6474b9510c3c543ea68f8878 +106f4067832a0bb1cab846b4224088d78c3a7965cf19f14de30448744a509093 diff --git a/12_MeshLoaders/bin/references/Stanford_Bunny.geomhash b/12_MeshLoaders/bin/references/Stanford_Bunny.geomhash index 0cb59bc94..750968b01 100644 --- a/12_MeshLoaders/bin/references/Stanford_Bunny.geomhash +++ b/12_MeshLoaders/bin/references/Stanford_Bunny.geomhash @@ -1 +1 @@ -77f984c2ffe3211196f4d0f87259607c7fca371ff3c22c6fd6f4d2f0800367b1 +9b063da4dd48cb276be30a55e64323418140fc88395b7753ff19edc452fb0253 diff --git a/12_MeshLoaders/bin/references/yellowflower.geomhash b/12_MeshLoaders/bin/references/yellowflower.geomhash index 90368fb9f..811367aec 100644 --- a/12_MeshLoaders/bin/references/yellowflower.geomhash +++ b/12_MeshLoaders/bin/references/yellowflower.geomhash @@ -1 +1 @@ -d30d7e4a7f79156fcb18682d871256ac52bc12497bc7f959a150a6191f1074fb +21405720f3f8f7be6310ff20913dee7b1d3a4a16095c4e6a7445de60338bee79 diff --git a/common/include/nbl/examples/common/GeometryAABBUtilities.h b/common/include/nbl/examples/common/GeometryAABBUtilities.h new file mode 100644 index 000000000..4f10e9a8c --- /dev/null +++ b/common/include/nbl/examples/common/GeometryAABBUtilities.h @@ -0,0 +1,145 @@ +// Copyright (C) 2018-2025 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h +#ifndef _NBL_EXAMPLES_COMMON_GEOMETRY_AABB_UTILITIES_INCLUDED_ +#define _NBL_EXAMPLES_COMMON_GEOMETRY_AABB_UTILITIES_INCLUDED_ + +#include "nbl/asset/ICPUPolygonGeometry.h" + +#include +#include +#include + +namespace nbl::examples::geometry +{ + +inline bool isFinite(const double value) +{ + return std::isfinite(value); +} + +inline bool isFinite(const hlsl::float64_t3& value) +{ + return isFinite(value.x) && isFinite(value.y) && isFinite(value.z); +} + +template +inline bool isValidAABB(const hlsl::shapes::AABB<3, Scalar>& aabb) +{ + return + std::isfinite(static_cast(aabb.minVx.x)) && + std::isfinite(static_cast(aabb.minVx.y)) && + std::isfinite(static_cast(aabb.minVx.z)) && + std::isfinite(static_cast(aabb.maxVx.x)) && + std::isfinite(static_cast(aabb.maxVx.y)) && + std::isfinite(static_cast(aabb.maxVx.z)) && + (aabb.minVx.x <= aabb.maxVx.x) && + (aabb.minVx.y <= aabb.maxVx.y) && + (aabb.minVx.z <= aabb.maxVx.z); +} + +template +inline hlsl::shapes::AABB<3, Scalar> translateAABB(const hlsl::shapes::AABB<3, Scalar>& aabb, const hlsl::vector& translation) +{ + auto out = aabb; + out.minVx += translation; + out.maxVx += translation; + return out; +} + +template +inline hlsl::shapes::AABB<3, Scalar> scaleAABB(const hlsl::shapes::AABB<3, Scalar>& aabb, const Scalar scale) +{ + auto out = aabb; + out.minVx *= scale; + out.maxVx *= scale; + return out; +} + +inline hlsl::shapes::AABB<3, double> computeFiniteUsedPositionAABB(const asset::ICPUPolygonGeometry* geometry) +{ + auto aabb = hlsl::shapes::AABB<3, double>::create(); + if (!geometry) + return aabb; + + const auto positionView = geometry->getPositionView(); + const uint64_t vertexCount = positionView.getElementCount(); + if (!vertexCount) + return aabb; + + const auto indexView = geometry->getIndexView(); + const uint64_t vertexRefCount = geometry->getVertexReferenceCount(); + bool hasFiniteVertex = false; + for (uint64_t i = 0u; i < vertexRefCount; ++i) + { + uint64_t vertexIx = i; + if (indexView) + { + const auto* const ptr = static_cast(indexView.getPointer(i)); + if (!ptr) + continue; + switch (indexView.composed.format) + { + case asset::EF_R16_UINT: + { + uint16_t index = 0u; + memcpy(&index, ptr, sizeof(index)); + vertexIx = index; + } + break; + case asset::EF_R32_UINT: + { + uint32_t index = 0u; + memcpy(&index, ptr, sizeof(index)); + vertexIx = index; + } + break; + default: + continue; + } + } + if (vertexIx >= vertexCount) + continue; + + hlsl::float32_t3 decoded = {}; + positionView.decodeElement(vertexIx, decoded); + const hlsl::float64_t3 point = { + static_cast(decoded.x), + static_cast(decoded.y), + static_cast(decoded.z) + }; + if (!isFinite(point)) + continue; + + if (!hasFiniteVertex) + { + aabb.minVx = point; + aabb.maxVx = point; + hasFiniteVertex = true; + continue; + } + + aabb.minVx.x = std::min(aabb.minVx.x, point.x); + aabb.minVx.y = std::min(aabb.minVx.y, point.y); + aabb.minVx.z = std::min(aabb.minVx.z, point.z); + aabb.maxVx.x = std::max(aabb.maxVx.x, point.x); + aabb.maxVx.y = std::max(aabb.maxVx.y, point.y); + aabb.maxVx.z = std::max(aabb.maxVx.z, point.z); + } + + if (!hasFiniteVertex) + return hlsl::shapes::AABB<3, double>::create(); + return aabb; +} + +inline hlsl::shapes::AABB<3, double> fallbackUnitAABB() +{ + hlsl::shapes::AABB<3, double> fallback = hlsl::shapes::AABB<3, double>::create(); + fallback.minVx = hlsl::float64_t3(-1.0, -1.0, -1.0); + fallback.maxVx = hlsl::float64_t3(1.0, 1.0, 1.0); + return fallback; +} + +} // namespace nbl::examples::geometry + +#endif diff --git a/common/include/nbl/examples/common/ImageComparison.h b/common/include/nbl/examples/common/ImageComparison.h new file mode 100644 index 000000000..fbfb9b095 --- /dev/null +++ b/common/include/nbl/examples/common/ImageComparison.h @@ -0,0 +1,63 @@ +// Copyright (C) 2018-2025 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h +#ifndef _NBL_EXAMPLES_COMMON_IMAGE_COMPARISON_INCLUDED_ +#define _NBL_EXAMPLES_COMMON_IMAGE_COMPARISON_INCLUDED_ + +#include "nbl/asset/ICPUImage.h" +#include "nbl/asset/ICPUImageView.h" + +namespace nbl::examples::image +{ + +inline bool compareCpuImageViewsByCodeUnit(const asset::ICPUImageView* a, const asset::ICPUImageView* b, uint64_t& diffCount, uint16_t& maxDiff) +{ + diffCount = 0u; + maxDiff = 0u; + if (!a || !b) + return false; + + const auto* imgA = a->getCreationParameters().image.get(); + const auto* imgB = b->getCreationParameters().image.get(); + if (!imgA || !imgB) + return false; + + const auto paramsA = imgA->getCreationParameters(); + const auto paramsB = imgB->getCreationParameters(); + if (paramsA.format != paramsB.format) + return false; + if (paramsA.extent != paramsB.extent) + return false; + + const auto* bufA = imgA->getBuffer(); + const auto* bufB = imgB->getBuffer(); + if (!bufA || !bufB) + return false; + + const size_t sizeA = bufA->getSize(); + if (sizeA != bufB->getSize()) + return false; + + const auto* dataA = static_cast(bufA->getPointer()); + const auto* dataB = static_cast(bufB->getPointer()); + if (!dataA || !dataB) + return false; + + for (size_t i = 0u; i < sizeA; ++i) + { + const int diff = static_cast(dataA[i]) - static_cast(dataB[i]); + const uint16_t absDiff = static_cast(diff < 0 ? -diff : diff); + if (!absDiff) + continue; + ++diffCount; + if (absDiff > maxDiff) + maxDiff = absDiff; + } + + return true; +} + +} // namespace nbl::examples::image + +#endif + diff --git a/common/include/nbl/examples/common/MonoWindowApplication.hpp b/common/include/nbl/examples/common/MonoWindowApplication.hpp index 7fd322e34..cc647d011 100644 --- a/common/include/nbl/examples/common/MonoWindowApplication.hpp +++ b/common/include/nbl/examples/common/MonoWindowApplication.hpp @@ -42,7 +42,7 @@ class MonoWindowApplication : public virtual SimpleWindowedApplication params.x = 32; params.y = 32; params.flags = ui::IWindow::ECF_HIDDEN | IWindow::ECF_BORDERLESS | IWindow::ECF_RESIZABLE | IWindow::ECF_CAN_MINIMIZE; - params.windowCaption = "MonoWindowApplication"; + params.windowCaption = getWindowCaption(); params.callback = windowCallback; const_cast&>(m_window) = m_winMgr->createWindow(std::move(params)); } @@ -70,6 +70,7 @@ class MonoWindowApplication : public virtual SimpleWindowedApplication return false; ISwapchain::SCreationParams swapchainParams = { .surface = smart_refctd_ptr(m_surface->getSurface()) }; + amendSwapchainCreateParams(swapchainParams); if (!swapchainParams.deduceFormat(m_physicalDevice)) return logFail("Could not choose a Surface Format for the Swapchain!"); @@ -140,6 +141,8 @@ class MonoWindowApplication : public virtual SimpleWindowedApplication // virtual inline bool keepRunning() override { + if (!shouldKeepRunning()) + return false; if (m_surface->irrecoverable()) return false; @@ -158,6 +161,17 @@ class MonoWindowApplication : public virtual SimpleWindowedApplication } protected: + virtual inline const char* getWindowCaption() const + { + return "MonoWindowApplication"; + } + virtual inline void amendSwapchainCreateParams(video::ISwapchain::SCreationParams&) const + { + } + virtual inline bool shouldKeepRunning() const + { + return true; + } inline void onAppInitializedFinish() { m_winMgr->show(m_window.get()); From 48945f7bee9e38de01378432ca7c72d3ba76eb49 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Thu, 5 Mar 2026 09:59:24 +0100 Subject: [PATCH 14/34] Refine mesh loader image compare code units --- 12_MeshLoaders/MeshLoadersApp.hpp | 10 ++- 12_MeshLoaders/MeshLoadersAppRuntime.cpp | 22 +++-- 12_MeshLoaders/README.md | 4 +- .../nbl/examples/common/ImageComparison.h | 85 ++++++++++++++++--- 4 files changed, 97 insertions(+), 24 deletions(-) diff --git a/12_MeshLoaders/MeshLoadersApp.hpp b/12_MeshLoaders/MeshLoadersApp.hpp index e9a08b202..5c414c7db 100644 --- a/12_MeshLoaders/MeshLoadersApp.hpp +++ b/12_MeshLoaders/MeshLoadersApp.hpp @@ -197,7 +197,11 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built bool validateWrittenAsset(const system::path& path); bool captureScreenshot(const system::path& path, core::smart_refctd_ptr& outImage); bool appendGeometriesFromBundle(const asset::SAssetBundle& bundle, core::vector>& out) const; - bool compareImages(const asset::ICPUImageView* a, const asset::ICPUImageView* b, uint64_t& diffCount, uint16_t& maxDiff); + bool compareImages( + const asset::ICPUImageView* a, + const asset::ICPUImageView* b, + uint64_t& diffCodeUnitCount, + uint32_t& maxDiffCodeUnitValue); void advanceCase(); bool shouldKeepRunning() const override; @@ -206,8 +210,8 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built constexpr static inline uint32_t CiFramesBeforeCapture = 10u; constexpr static inline uint32_t NonCiFramesPerCase = 120u; constexpr static inline uint32_t RowViewFramesBeforeCapture = 10u; - constexpr static inline uint64_t MaxImageDiffBytes = 16u; - constexpr static inline uint16_t MaxImageDiffValue = 1u; + constexpr static inline uint64_t MaxImageDiffCodeUnits = 16u; + constexpr static inline uint32_t MaxImageDiffCodeUnitValue = 1u; smart_refctd_ptr m_renderer; smart_refctd_ptr m_semaphore; diff --git a/12_MeshLoaders/MeshLoadersAppRuntime.cpp b/12_MeshLoaders/MeshLoadersAppRuntime.cpp index a121db615..3deb85e72 100644 --- a/12_MeshLoaders/MeshLoadersAppRuntime.cpp +++ b/12_MeshLoaders/MeshLoadersAppRuntime.cpp @@ -364,9 +364,13 @@ bool MeshLoadersApp::appendGeometriesFromBundle(const asset::SAssetBundle& bundl return !out.empty(); } -bool MeshLoadersApp::compareImages(const asset::ICPUImageView* a, const asset::ICPUImageView* b, uint64_t& diffCount, uint16_t& maxDiff) +bool MeshLoadersApp::compareImages( + const asset::ICPUImageView* a, + const asset::ICPUImageView* b, + uint64_t& diffCodeUnitCount, + uint32_t& maxDiffCodeUnitValue) { - return nbl::examples::image::compareCpuImageViewsByCodeUnit(a, b, diffCount, maxDiff); + return nbl::examples::image::compareCpuImageViewsByCodeUnit(a, b, diffCodeUnitCount, maxDiffCodeUnitValue); } void MeshLoadersApp::advanceCase() @@ -427,14 +431,14 @@ void MeshLoadersApp::advanceCase() failExit("Geometry hash mismatch for %s. Current=%s Reference=%s ReferenceFile=%s", m_caseName.c_str(), geometryHashToHex(writtenHash).c_str(), geometryHashToHex(m_referenceGeometryHash).c_str(), m_caseGeometryHashReferencePath.empty() ? "" : m_caseGeometryHashReferencePath.string().c_str()); } - uint64_t diffCount = 0u; - uint16_t maxDiff = 0u; - if (!compareImages(m_loadedScreenshot.get(), m_writtenScreenshot.get(), diffCount, maxDiff)) + uint64_t diffCodeUnitCount = 0u; + uint32_t maxDiffCodeUnitValue = 0u; + if (!compareImages(m_loadedScreenshot.get(), m_writtenScreenshot.get(), diffCodeUnitCount, maxDiffCodeUnitValue)) failExit("Image compare failed for %s.", m_caseName.c_str()); - if (diffCount > MaxImageDiffBytes || maxDiff > MaxImageDiffValue) - failExit("Image diff detected for %s. Bytes: %llu MaxDiff: %u", m_caseName.c_str(), static_cast(diffCount), maxDiff); - if (diffCount != 0u) - m_logger->log("Image diff within tolerance for %s. Bytes: %llu MaxDiff: %u", ILogger::ELL_WARNING, m_caseName.c_str(), static_cast(diffCount), maxDiff); + if (diffCodeUnitCount > MaxImageDiffCodeUnits || maxDiffCodeUnitValue > MaxImageDiffCodeUnitValue) + failExit("Image diff detected for %s. CodeUnits: %llu MaxCodeUnitDiff: %u", m_caseName.c_str(), static_cast(diffCodeUnitCount), maxDiffCodeUnitValue); + if (diffCodeUnitCount != 0u) + m_logger->log("Image diff within tolerance for %s. CodeUnits: %llu MaxCodeUnitDiff: %u", ILogger::ELL_WARNING, m_caseName.c_str(), static_cast(diffCodeUnitCount), maxDiffCodeUnitValue); advanceToNextCase(); } diff --git a/12_MeshLoaders/README.md b/12_MeshLoaders/README.md index fafccf661..5b47c17a5 100644 --- a/12_MeshLoaders/README.md +++ b/12_MeshLoaders/README.md @@ -127,8 +127,8 @@ Rules: - Per-case geometry hash: - deterministic `BLAKE3` hash compared with `references/*.geomhash` - Per-case image consistency: - - `*_loaded.png` vs `*_written.png` byte diff - - thresholds come from `MaxImageDiffBytes` and `MaxImageDiffValue` in `MeshLoadersApp.hpp` + - `*_loaded.png` vs `*_written.png` code-unit diff + - thresholds come from `MaxImageDiffCodeUnits` and `MaxImageDiffCodeUnitValue` in `MeshLoadersApp.hpp` - Any mismatch ends with non-zero exit code ## Performance logs to trust diff --git a/common/include/nbl/examples/common/ImageComparison.h b/common/include/nbl/examples/common/ImageComparison.h index fbfb9b095..aabb31e94 100644 --- a/common/include/nbl/examples/common/ImageComparison.h +++ b/common/include/nbl/examples/common/ImageComparison.h @@ -6,14 +6,47 @@ #include "nbl/asset/ICPUImage.h" #include "nbl/asset/ICPUImageView.h" +#include "nbl/asset/format/EFormat.h" + +#include namespace nbl::examples::image { -inline bool compareCpuImageViewsByCodeUnit(const asset::ICPUImageView* a, const asset::ICPUImageView* b, uint64_t& diffCount, uint16_t& maxDiff) +struct SCodeUnitDiff +{ + static inline uint32_t resolveCodeUnitBytes(const asset::E_FORMAT format) + { + if (asset::isBlockCompressionFormat(format)) + return 1u; + const uint32_t channels = asset::getFormatChannelCount(format); + if (!channels) + return 1u; + const uint32_t texelBytes = asset::getTexelOrBlockBytesize(format); + if (!texelBytes || (texelBytes % channels) != 0u) + return 1u; + const uint32_t bytesPerChannel = texelBytes / channels; + if (bytesPerChannel == 1u || bytesPerChannel == 2u || bytesPerChannel == 4u) + return bytesPerChannel; + return 1u; + } + + template + static inline uint32_t absoluteDiff(const T a, const T b) + { + return (a >= b) ? static_cast(a - b) : static_cast(b - a); + } +}; + +inline bool compareCpuImageViewsByCodeUnit( + const asset::ICPUImageView* a, + const asset::ICPUImageView* b, + uint64_t& diffCodeUnitCount, + uint32_t& maxDiffCodeUnitValue +) { - diffCount = 0u; - maxDiff = 0u; + diffCodeUnitCount = 0u; + maxDiffCodeUnitValue = 0u; if (!a || !b) return false; @@ -43,15 +76,48 @@ inline bool compareCpuImageViewsByCodeUnit(const asset::ICPUImageView* a, const if (!dataA || !dataB) return false; - for (size_t i = 0u; i < sizeA; ++i) + const uint32_t codeUnitBytes = SCodeUnitDiff::resolveCodeUnitBytes(paramsA.format); + const size_t comparableSize = sizeA - (sizeA % codeUnitBytes); + for (size_t i = 0u; i < comparableSize; i += codeUnitBytes) { - const int diff = static_cast(dataA[i]) - static_cast(dataB[i]); - const uint16_t absDiff = static_cast(diff < 0 ? -diff : diff); + uint32_t absDiff = 0u; + if (codeUnitBytes == 1u) + { + const uint8_t va = dataA[i]; + const uint8_t vb = dataB[i]; + absDiff = SCodeUnitDiff::absoluteDiff(va, vb); + } + else if (codeUnitBytes == 2u) + { + uint16_t va = 0u; + uint16_t vb = 0u; + std::memcpy(&va, dataA + i, sizeof(va)); + std::memcpy(&vb, dataB + i, sizeof(vb)); + absDiff = SCodeUnitDiff::absoluteDiff(va, vb); + } + else + { + uint32_t va = 0u; + uint32_t vb = 0u; + std::memcpy(&va, dataA + i, sizeof(va)); + std::memcpy(&vb, dataB + i, sizeof(vb)); + absDiff = SCodeUnitDiff::absoluteDiff(va, vb); + } if (!absDiff) continue; - ++diffCount; - if (absDiff > maxDiff) - maxDiff = absDiff; + ++diffCodeUnitCount; + if (absDiff > maxDiffCodeUnitValue) + maxDiffCodeUnitValue = absDiff; + } + + for (size_t i = comparableSize; i < sizeA; ++i) + { + const uint32_t absDiff = SCodeUnitDiff::absoluteDiff(dataA[i], dataB[i]); + if (!absDiff) + continue; + ++diffCodeUnitCount; + if (absDiff > maxDiffCodeUnitValue) + maxDiffCodeUnitValue = absDiff; } return true; @@ -60,4 +126,3 @@ inline bool compareCpuImageViewsByCodeUnit(const asset::ICPUImageView* a, const } // namespace nbl::examples::image #endif - From 391e4ebd9d29a3be501a714a1cc340c4b3d8bc04 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Thu, 5 Mar 2026 15:41:43 +0100 Subject: [PATCH 15/34] Simplify MeshLoaders runtime tuning and hash checks --- 12_MeshLoaders/CMakeLists.txt | 30 --- 12_MeshLoaders/MeshLoadersApp.hpp | 17 -- 12_MeshLoaders/MeshLoadersAppLifecycle.cpp | 206 ++------------------- 12_MeshLoaders/MeshLoadersAppLoad.cpp | 2 - 12_MeshLoaders/MeshLoadersAppRuntime.cpp | 185 ------------------ 12_MeshLoaders/README.md | 27 +-- 6 files changed, 19 insertions(+), 448 deletions(-) diff --git a/12_MeshLoaders/CMakeLists.txt b/12_MeshLoaders/CMakeLists.txt index 42d992cd9..566bf07d1 100644 --- a/12_MeshLoaders/CMakeLists.txt +++ b/12_MeshLoaders/CMakeLists.txt @@ -10,7 +10,6 @@ set(SRCs option(NBL_MESHLOADERS_ENABLE_BENCHMARK_DATASETS "Enable benchmark dataset clone + benchmark payload setup for 12_MeshLoaders." OFF) option(NBL_MESHLOADERS_DEFAULT_START_WITH_BENCHMARK_TESTLIST "When benchmark datasets are enabled, use benchmark payload as default startup test list in batch mode." OFF) -option(NBL_MESHLOADERS_ENABLE_HASH_CTESTS "Enable hash parity CTests (legacy_seq/new_seq/new_parallel) for 12_MeshLoaders." OFF) set(NBL_MESHLOADERS_BENCHMARK_DATASET_DIR "${CMAKE_BINARY_DIR}/meshloaders_benchmark_datasets" CACHE PATH "Destination directory for cloned 12_MeshLoaders benchmark datasets.") set(NBL_MESHLOADERS_BENCHMARK_DATASET_REPO "https://github.com/Devsh-Graphics-Programming/Nabla-Benchmark-Datasets.git" CACHE STRING "Git repository URL for 12_MeshLoaders benchmark datasets.") set(NBL_MESHLOADERS_BENCHMARK_PAYLOAD_RELATIVE_PATH "inputs_benchmark.json" CACHE STRING "Relative path to committed benchmark payload JSON inside dataset repo.") @@ -99,39 +98,10 @@ add_test(NAME NBL_MESHLOADERS_CI COMMAND_EXPAND_LISTS ) -if (NBL_MESHLOADERS_ENABLE_HASH_CTESTS) - set(_NBL_MESHLOADERS_HASH_TEST_COMMON_ARGS - --hash-test - ) - - if (NBL_MESHLOADERS_ENABLE_BENCHMARK_DATASETS) - list(APPEND _NBL_MESHLOADERS_HASH_TEST_COMMON_ARGS --testlist "${NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON}") - endif() - - add_test(NAME NBL_MESHLOADERS_HASH_TEST_HEURISTIC - COMMAND "$" ${_NBL_MESHLOADERS_HASH_TEST_COMMON_ARGS} --runtime-tuning heuristic - WORKING_DIRECTORY "$" - COMMAND_EXPAND_LISTS - ) - add_test(NAME NBL_MESHLOADERS_HASH_TEST_HYBRID - COMMAND "$" ${_NBL_MESHLOADERS_HASH_TEST_COMMON_ARGS} --runtime-tuning hybrid - WORKING_DIRECTORY "$" - COMMAND_EXPAND_LISTS - ) - set_tests_properties( - NBL_MESHLOADERS_HASH_TEST_HEURISTIC - NBL_MESHLOADERS_HASH_TEST_HYBRID - PROPERTIES - LABELS "meshloaders;hash;ci" - ) -endif() - if (NBL_MESHLOADERS_ENABLE_BENCHMARK_DATASETS) set(_NBL_MESHLOADERS_BENCHMARK_CI_COMMON_ARGS --ci - --update-references --testlist "${NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON}" - --loader-content-hashes ) add_test(NAME NBL_MESHLOADERS_CI_BENCHMARK_HEURISTIC diff --git a/12_MeshLoaders/MeshLoadersApp.hpp b/12_MeshLoaders/MeshLoadersApp.hpp index 5c414c7db..2a43d12a0 100644 --- a/12_MeshLoaders/MeshLoadersApp.hpp +++ b/12_MeshLoaders/MeshLoadersApp.hpp @@ -151,14 +151,6 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built bool parseCommandLineOptions(const system::path& effectiveInputCWD, const system::path& effectiveOutputCWD, const system::path& defaultBenchmarkTestListPath); std::string makeCaptionModelPath() const; - static std::string sanitizeCaseNameForFilename(std::string name); - system::path getGeometryHashReferencePath(const std::string& caseName) const; - static std::string geometryHashToHex(const core::blake3_hash_t& hash); - static bool tryParseNibble(char c, uint8_t& out); - static bool tryParseGeometryHashHex(std::string hex, core::blake3_hash_t& outHash); - bool readGeometryHashReference(const system::path& refPath, core::blake3_hash_t& outHash) const; - bool writeGeometryHashReference(const system::path& refPath, const core::blake3_hash_t& hash) const; - bool startCase(size_t index); bool advanceToNextCase(); void reloadInteractive(); @@ -170,7 +162,6 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built bool loadModel(const system::path& modelPath, bool updateCamera, bool storeCamera); bool loadRowView(RowViewReloadMode mode); bool writeGeometry(smart_refctd_ptr geometry, const std::string& savePath); - bool runHashConsistencyChecks(); void setupCameraFromAABB(const hlsl::shapes::AABB<3, double>& bound); @@ -193,7 +184,6 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built void logRowViewAssetLoad(const system::path& path, double ms, bool cached) const; void logRowViewLoadTotal(double ms, size_t hits, size_t misses) const; - core::blake3_hash_t hashGeometry(const ICPUPolygonGeometry* geo); bool validateWrittenAsset(const system::path& path); bool captureScreenshot(const system::path& path, core::smart_refctd_ptr& outImage); bool appendGeometriesFromBundle(const asset::SAssetBundle& bundle, core::vector>& out) const; @@ -248,16 +238,11 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built nbl::system::path m_screenshotPrefixPath; nbl::system::path m_rowViewScreenshotPath; nbl::system::path m_testListPath; - nbl::system::path m_geometryHashReferenceDir; - nbl::system::path m_caseGeometryHashReferencePath; std::optional m_loaderPerfLogPath; std::optional m_rowAddPath; uint32_t m_rowDuplicateCount = 0u; smart_refctd_ptr m_assetLoadLogger; smart_refctd_ptr m_loaderPerfLogger; - bool m_updateGeometryHashReferences = false; - bool m_forceLoaderContentHashes = true; - bool m_hashTestOnly = false; asset::SFileIOPolicy::SRuntimeTuning::Mode m_runtimeTuningMode = asset::SFileIOPolicy::SRuntimeTuning::Mode::Heuristic; RunMode m_runMode = RunMode::Batch; @@ -274,8 +259,6 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built nbl::system::path m_writtenScreenshotPath; core::smart_refctd_ptr m_currentCpuGeom; - core::blake3_hash_t m_referenceGeometryHash = {}; - bool m_hasReferenceGeometryHash = false; core::smart_refctd_ptr m_loadedScreenshot; core::smart_refctd_ptr m_writtenScreenshot; diff --git a/12_MeshLoaders/MeshLoadersAppLifecycle.cpp b/12_MeshLoaders/MeshLoadersAppLifecycle.cpp index 3c533cbd8..bde919107 100644 --- a/12_MeshLoaders/MeshLoadersAppLifecycle.cpp +++ b/12_MeshLoaders/MeshLoadersAppLifecycle.cpp @@ -13,7 +13,6 @@ #include #include #include -#include #ifdef NBL_BUILD_MITSUBA_LOADER #include "nbl/ext/MitsubaLoader/CSerializedLoader.h" @@ -146,9 +145,6 @@ bool MeshLoadersApp::parseCommandLineOptions(const system::path& effectiveInputC parser.add_argument("--ci") .help("Run in CI mode: load test list, write .ply, capture screenshots, compare data, and exit.") .flag(); - parser.add_argument("--hash-test") - .help("Run headless prehash consistency check: invalid before recompute, valid after recompute, then exit.") - .flag(); parser.add_argument("--interactive") .help("Use file dialog to select a single model.") .flag(); @@ -164,15 +160,9 @@ bool MeshLoadersApp::parseCommandLineOptions(const system::path& effectiveInputC parser.add_argument("--loader-perf-log") .nargs(1) .help("Write loader diagnostics to a file instead of stdout."); - parser.add_argument("--loader-content-hashes") - .help("Force loaders to compute CPU buffer content hashes before returning. Enabled by default.") - .flag(); parser.add_argument("--runtime-tuning") .nargs(1) - .help("Runtime tuning mode for loaders: none|heuristic|hybrid. Default: heuristic."); - parser.add_argument("--update-references") - .help("Update or create geometry hash references for CI validation.") - .flag(); + .help("Runtime tuning mode for loaders: sequential|heuristic|hybrid. Default: heuristic."); try { @@ -189,11 +179,6 @@ bool MeshLoadersApp::parseCommandLineOptions(const system::path& effectiveInputC m_runMode = RunMode::Interactive; if (parser["--ci"] == true) m_runMode = RunMode::CI; - if (parser["--hash-test"] == true) - { - m_hashTestOnly = true; - m_runMode = RunMode::CI; - } const bool hasExplicitTestListArg = parser.present("--testlist").has_value(); if (parser.present("--savepath")) @@ -254,22 +239,18 @@ bool MeshLoadersApp::parseCommandLineOptions(const system::path& effectiveInputC tmp = effectiveOutputCWD / tmp; m_loaderPerfLogPath = tmp; } - if (parser["--update-references"] == true) - m_updateGeometryHashReferences = true; - if (parser["--loader-content-hashes"] == true) - m_forceLoaderContentHashes = true; if (parser.present("--runtime-tuning")) { auto mode = parser.get("--runtime-tuning"); std::transform(mode.begin(), mode.end(), mode.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); - if (mode == "none") - m_runtimeTuningMode = asset::SFileIOPolicy::SRuntimeTuning::Mode::None; + if (mode == "sequential" || mode == "none") + m_runtimeTuningMode = asset::SFileIOPolicy::SRuntimeTuning::Mode::Sequential; else if (mode == "heuristic") m_runtimeTuningMode = asset::SFileIOPolicy::SRuntimeTuning::Mode::Heuristic; else if (mode == "hybrid") m_runtimeTuningMode = asset::SFileIOPolicy::SRuntimeTuning::Mode::Hybrid; else - return logFail("Invalid --runtime-tuning value. Expected: none|heuristic|hybrid."); + return logFail("Invalid --runtime-tuning value. Expected: sequential|heuristic|hybrid."); } return true; @@ -295,23 +276,6 @@ bool MeshLoadersApp::onAppInitialized(smart_refctd_ptr&& system) if (!parseCommandLineOptions(effectiveInputCWD, effectiveOutputCWD, defaultBenchmarkTestListPath)) return false; - const path inputReferencesDir = effectiveInputCWD / "references"; - const path outputReferencesDir = effectiveOutputCWD / "references"; - std::error_code referenceDirEc; - const bool hasInputReferencesDir = std::filesystem::is_directory(inputReferencesDir, referenceDirEc) && !referenceDirEc; - referenceDirEc.clear(); - const bool hasOutputReferencesDir = std::filesystem::is_directory(outputReferencesDir, referenceDirEc) && !referenceDirEc; - m_geometryHashReferenceDir = hasOutputReferencesDir || !hasInputReferencesDir ? outputReferencesDir : inputReferencesDir; - if (hasOutputReferencesDir && !hasInputReferencesDir) - m_logger->log("Geometry hash references resolved to output directory: %s", system::ILogger::ELL_INFO, m_geometryHashReferenceDir.string().c_str()); - if (m_runMode == RunMode::CI || m_updateGeometryHashReferences) - { - std::error_code ec; - std::filesystem::create_directories(m_geometryHashReferenceDir, ec); - if (ec) - return logFail("Failed to create geometry hash reference directory: %s", m_geometryHashReferenceDir.string().c_str()); - } - if (m_saveGeom) std::filesystem::create_directories(m_saveGeomPrefixPath); std::filesystem::create_directories(m_screenshotPrefixPath); @@ -342,32 +306,22 @@ bool MeshLoadersApp::onAppInitialized(smart_refctd_ptr&& system) return logFail("Failed to create renderer!"); #ifdef NBL_BUILD_DEBUG_DRAW - if (!m_hashTestOnly) - { - auto* renderpass = scRes->getRenderpass(); - ext::debug_draw::DrawAABB::SCreationParameters params = {}; - params.assetManager = m_assetMgr; - params.transfer = getTransferUpQueue(); - params.drawMode = ext::debug_draw::DrawAABB::ADM_DRAW_BATCH; - params.batchPipelineLayout = ext::debug_draw::DrawAABB::createDefaultPipelineLayout(m_device.get()); - params.renderpass = smart_refctd_ptr(renderpass); - params.utilities = m_utils; - m_drawAABB = ext::debug_draw::DrawAABB::create(std::move(params)); - } + { + auto* renderpass = scRes->getRenderpass(); + ext::debug_draw::DrawAABB::SCreationParameters params = {}; + params.assetManager = m_assetMgr; + params.transfer = getTransferUpQueue(); + params.drawMode = ext::debug_draw::DrawAABB::ADM_DRAW_BATCH; + params.batchPipelineLayout = ext::debug_draw::DrawAABB::createDefaultPipelineLayout(m_device.get()); + params.renderpass = smart_refctd_ptr(renderpass); + params.utilities = m_utils; + m_drawAABB = ext::debug_draw::DrawAABB::create(std::move(params)); + } #endif if (!initTestCases()) return false; - if (m_hashTestOnly) - { - if (!runHashConsistencyChecks()) - return false; - m_shouldQuit = true; - onAppInitializedFinish(); - return true; - } - if (isRowViewActive()) { m_nonInteractiveTest = false; @@ -780,98 +734,6 @@ system::path MeshLoadersApp::resolveSavePath(const system::path& modelPath) cons return m_saveGeomPrefixPath / (stem + "_written" + ext); } -std::string MeshLoadersApp::sanitizeCaseNameForFilename(std::string name) -{ - for (auto& ch : name) - { - const unsigned char uch = static_cast(ch); - if (!(std::isalnum(uch) || ch == '_' || ch == '-' || ch == '.')) - ch = '_'; - } - if (name.empty()) - name = "unnamed_case"; - return name; -} - -system::path MeshLoadersApp::getGeometryHashReferencePath(const std::string& caseName) const -{ - return m_geometryHashReferenceDir / (sanitizeCaseNameForFilename(caseName) + ".geomhash"); -} - -std::string MeshLoadersApp::geometryHashToHex(const core::blake3_hash_t& hash) -{ - static constexpr char HexDigits[] = "0123456789abcdef"; - std::string out; - out.resize(sizeof(hash.data) * 2ull); - for (size_t i = 0ull; i < sizeof(hash.data); ++i) - { - const uint8_t v = hash.data[i]; - out[2ull * i + 0ull] = HexDigits[(v >> 4) & 0xfu]; - out[2ull * i + 1ull] = HexDigits[v & 0xfu]; - } - return out; -} - -bool MeshLoadersApp::tryParseNibble(const char c, uint8_t& out) -{ - if (c >= '0' && c <= '9') - { - out = static_cast(c - '0'); - return true; - } - if (c >= 'a' && c <= 'f') - { - out = static_cast(10 + c - 'a'); - return true; - } - if (c >= 'A' && c <= 'F') - { - out = static_cast(10 + c - 'A'); - return true; - } - return false; -} - -bool MeshLoadersApp::tryParseGeometryHashHex(std::string hex, core::blake3_hash_t& outHash) -{ - hex.erase(std::remove_if(hex.begin(), hex.end(), [](unsigned char c) { return std::isspace(c) != 0; }), hex.end()); - if (hex.size() != sizeof(outHash.data) * 2ull) - return false; - - for (size_t i = 0ull; i < sizeof(outHash.data); ++i) - { - uint8_t hi = 0u; - uint8_t lo = 0u; - if (!tryParseNibble(hex[2ull * i + 0ull], hi) || !tryParseNibble(hex[2ull * i + 1ull], lo)) - return false; - outHash.data[i] = static_cast((hi << 4) | lo); - } - return true; -} - -bool MeshLoadersApp::readGeometryHashReference(const system::path& refPath, core::blake3_hash_t& outHash) const -{ - std::ifstream in(refPath); - if (!in.is_open()) - return false; - std::string line; - std::getline(in, line); - return tryParseGeometryHashHex(std::move(line), outHash); -} - -bool MeshLoadersApp::writeGeometryHashReference(const system::path& refPath, const core::blake3_hash_t& hash) const -{ - std::error_code ec; - std::filesystem::create_directories(refPath.parent_path(), ec); - if (ec) - return false; - std::ofstream out(refPath, std::ios::binary | std::ios::trunc); - if (!out.is_open()) - return false; - out << geometryHashToHex(hash) << '\n'; - return out.good(); -} - bool MeshLoadersApp::startCase(const size_t index) { if (index >= m_cases.size()) @@ -883,8 +745,6 @@ bool MeshLoadersApp::startCase(const size_t index) m_loadedScreenshot = nullptr; m_writtenScreenshot = nullptr; m_referenceCamera.reset(); - m_hasReferenceGeometryHash = false; - m_caseGeometryHashReferencePath.clear(); const auto& testCase = m_cases[m_caseIndex]; m_caseName = testCase.name.empty() ? testCase.path.stem().string() : testCase.name; @@ -895,42 +755,6 @@ bool MeshLoadersApp::startCase(const size_t index) if (!loadModel(testCase.path, true, true)) return false; - if (m_currentCpuGeom) - { - const auto loadedGeometryHash = hashGeometry(m_currentCpuGeom.get()); - m_referenceGeometryHash = loadedGeometryHash; - m_hasReferenceGeometryHash = true; - m_caseGeometryHashReferencePath = getGeometryHashReferencePath(m_caseName); - - if (m_updateGeometryHashReferences) - { - const bool referenceExisted = std::filesystem::exists(m_caseGeometryHashReferencePath); - if (!writeGeometryHashReference(m_caseGeometryHashReferencePath, loadedGeometryHash)) - return logFail("Failed to write geometry hash reference: %s", m_caseGeometryHashReferencePath.string().c_str()); - if (!referenceExisted) - m_logger->log("Geometry hash reference did not exist for %s. Created new reference at %s", ILogger::ELL_WARNING, m_caseName.c_str(), m_caseGeometryHashReferencePath.string().c_str()); - else - m_logger->log("Geometry hash reference updated for %s at %s", ILogger::ELL_INFO, m_caseName.c_str(), m_caseGeometryHashReferencePath.string().c_str()); - } - else if (m_runMode == RunMode::CI) - { - if (!std::filesystem::exists(m_caseGeometryHashReferencePath)) - return logFail("Missing geometry hash reference for %s at %s. Run once with --update-references.", m_caseName.c_str(), m_caseGeometryHashReferencePath.string().c_str()); - - core::blake3_hash_t onDiskHash = {}; - if (!readGeometryHashReference(m_caseGeometryHashReferencePath, onDiskHash)) - return logFail("Invalid geometry hash reference for %s at %s", m_caseName.c_str(), m_caseGeometryHashReferencePath.string().c_str()); - - m_referenceGeometryHash = onDiskHash; - m_hasReferenceGeometryHash = true; - if (loadedGeometryHash != onDiskHash) - { - m_logger->log("Loaded geometry hash mismatch for %s. Current=%s Reference=%s", ILogger::ELL_ERROR, m_caseName.c_str(), geometryHashToHex(loadedGeometryHash).c_str(), geometryHashToHex(onDiskHash).c_str()); - return logFail("Loaded asset differs from stored geometry hash reference for %s.", m_caseName.c_str()); - } - } - } - return true; } diff --git a/12_MeshLoaders/MeshLoadersAppLoad.cpp b/12_MeshLoaders/MeshLoadersAppLoad.cpp index 88f5a9ee6..3c545e08f 100644 --- a/12_MeshLoaders/MeshLoadersAppLoad.cpp +++ b/12_MeshLoaders/MeshLoadersAppLoad.cpp @@ -768,8 +768,6 @@ IAssetLoader::SAssetLoadParams MeshLoadersApp::makeLoadParams() const params.logger = nullptr; params.cacheFlags = IAssetLoader::ECF_DUPLICATE_TOP_LEVEL; params.ioPolicy.runtimeTuning.mode = m_runtimeTuningMode; - if (!m_forceLoaderContentHashes) - params.loaderFlags = static_cast(params.loaderFlags | IAssetLoader::ELPF_DONT_COMPUTE_CONTENT_HASHES); return params; } diff --git a/12_MeshLoaders/MeshLoadersAppRuntime.cpp b/12_MeshLoaders/MeshLoadersAppRuntime.cpp index 3deb85e72..681b34144 100644 --- a/12_MeshLoaders/MeshLoadersAppRuntime.cpp +++ b/12_MeshLoaders/MeshLoadersAppRuntime.cpp @@ -4,10 +4,7 @@ #include "MeshLoadersApp.hpp" -#include "nbl/asset/IPreHashed.h" #include "nbl/ext/ScreenShot/ScreenShot.h" -#include "nbl/asset/interchange/SGeometryContentHashCommon.h" -#include "nbl/core/hash/blake.h" #include "nbl/examples/common/ImageComparison.h" std::string MeshLoadersApp::makeUniqueCaseName(const system::path& path) @@ -85,181 +82,6 @@ void MeshLoadersApp::logRowViewLoadTotal(const double ms, const size_t hits, con static_cast(misses)); } -core::blake3_hash_t MeshLoadersApp::hashGeometry(const ICPUPolygonGeometry* geo) -{ - if (!geo) - return asset::IPreHashed::INVALID_HASH; - - auto* mutableGeo = const_cast(geo); - CPolygonGeometryManipulator::recomputeContentHashes(mutableGeo); - - core::vector> buffers; - asset::SPolygonGeometryContentHash::collectBuffers(mutableGeo, buffers); - if (buffers.empty()) - return asset::IPreHashed::INVALID_HASH; - - core::blake3_hasher hasher; - if (const auto* indexing = geo->getIndexingCallback(); indexing) - { - hasher << indexing->degree(); - hasher << indexing->rate(); - hasher << indexing->knownTopology(); - } - for (const auto& buffer : buffers) - { - if (!buffer) - continue; - hasher << buffer->getContentHash(); - } - return static_cast(hasher); -} - -bool MeshLoadersApp::runHashConsistencyChecks() -{ - using clock_t = std::chrono::high_resolution_clock; - - if (m_cases.empty()) - return logFail("Hash test requires at least one test case."); - - IAssetLoader::SAssetLoadParams params = makeLoadParams(); - params.logger = nullptr; - params.loaderFlags = static_cast(params.loaderFlags | IAssetLoader::ELPF_DONT_COMPUTE_CONTENT_HASHES); - - double totalLoadMs = 0.0; - uint64_t totalGeometryCount = 0ull; - uint64_t totalBufferCount = 0ull; - uint64_t totalInvalidBefore = 0ull; - - for (const auto& testCase : m_cases) - { - m_assetMgr->clearAllAssetCache(); - - AssetLoadCallResult loadResult = {}; - if (!loadAssetCallFromPath(testCase.path, params, loadResult)) - failExit("Hash test failed to load input %s.", testCase.path.string().c_str()); - totalLoadMs += loadResult.getAssetMs; - - if (loadResult.bundle.getContents().empty()) - failExit("Hash test loaded empty asset for %s.", testCase.path.string().c_str()); - - core::vector> geometries; - if (!appendGeometriesFromBundle(loadResult.bundle, geometries)) - failExit("Hash test found no polygon geometry in %s.", testCase.path.string().c_str()); - - uint64_t caseBufferCount = 0ull; - uint64_t caseInvalidBefore = 0ull; - - for (size_t geoIx = 0u; geoIx < geometries.size(); ++geoIx) - { - auto* geometry = const_cast(geometries[geoIx].get()); - if (!geometry) - failExit("Hash test failed to access geometry %llu in %s.", static_cast(geoIx), testCase.path.string().c_str()); - - core::vector> buffers; - asset::SPolygonGeometryContentHash::collectBuffers(geometry, buffers); - if (buffers.empty()) - continue; - - for (const auto& buffer : buffers) - { - if (!buffer) - continue; - if (buffer->getContentHash() != asset::IPreHashed::INVALID_HASH) - failExit("Hash test expected invalid prehash for %s geo=%llu.", testCase.path.string().c_str(), static_cast(geoIx)); - ++caseInvalidBefore; - } - - const auto recomputeStart = clock_t::now(); - const auto aggregateHash = hashGeometry(geometry); - const auto recomputeMs = toMs(clock_t::now() - recomputeStart); - if (aggregateHash == asset::IPreHashed::INVALID_HASH) - failExit("Hash test recompute failed for %s geo=%llu.", testCase.path.string().c_str(), static_cast(geoIx)); - - for (size_t bufferIx = 0u; bufferIx < buffers.size(); ++bufferIx) - { - const auto& buffer = buffers[bufferIx]; - if (!buffer) - continue; - - if (buffer->getContentHash() == asset::IPreHashed::INVALID_HASH) - failExit("Hash test buffer still invalid for %s geo=%llu buffer=%llu.", testCase.path.string().c_str(), static_cast(geoIx), static_cast(bufferIx)); - - const auto* const ptr = buffer->getPointer(); - const size_t size = buffer->getSize(); - if (!ptr || size == 0ull) - continue; - - const auto expected = core::blake3_hash_buffer(ptr, size); - if (expected != buffer->getContentHash()) - { - failExit( - "Hash test mismatch for %s geo=%llu buffer=%llu expected=%s actual=%s", - testCase.path.string().c_str(), - static_cast(geoIx), - static_cast(bufferIx), - geometryHashToHex(expected).c_str(), - geometryHashToHex(buffer->getContentHash()).c_str()); - } - } - - const auto aggregateHashRepeat = hashGeometry(geometry); - if (aggregateHashRepeat != aggregateHash) - { - failExit( - "Hash test aggregate instability for %s geo=%llu first=%s second=%s", - testCase.path.string().c_str(), - static_cast(geoIx), - geometryHashToHex(aggregateHash).c_str(), - geometryHashToHex(aggregateHashRepeat).c_str()); - } - - if (m_logger) - { - m_logger->log( - "Hash test geometry: %s geo=%llu buffers=%llu recompute=%.3f ms aggregate=%s", - ILogger::ELL_INFO, - testCase.path.string().c_str(), - static_cast(geoIx), - static_cast(buffers.size()), - recomputeMs, - geometryHashToHex(aggregateHash).c_str()); - } - - caseBufferCount += buffers.size(); - ++totalGeometryCount; - } - - totalBufferCount += caseBufferCount; - totalInvalidBefore += caseInvalidBefore; - - if (m_logger) - { - m_logger->log( - "Hash test case: %s load=%.3f ms geos=%llu buffers=%llu invalid_before=%llu", - ILogger::ELL_INFO, - testCase.path.string().c_str(), - loadResult.getAssetMs, - static_cast(geometries.size()), - static_cast(caseBufferCount), - static_cast(caseInvalidBefore)); - } - } - - if (m_logger) - { - m_logger->log( - "Hash test summary: cases=%llu geos=%llu buffers=%llu invalid_before=%llu load=%.3f ms", - ILogger::ELL_INFO, - static_cast(m_cases.size()), - static_cast(totalGeometryCount), - static_cast(totalBufferCount), - static_cast(totalInvalidBefore), - totalLoadMs); - } - - return true; -} - bool MeshLoadersApp::validateWrittenAsset(const system::path& path) { if (!std::filesystem::exists(path)) @@ -424,13 +246,6 @@ void MeshLoadersApp::advanceCase() if (!captureScreenshot(m_writtenScreenshotPath, m_writtenScreenshot)) failExit("Failed to capture written screenshot."); - if (m_hasReferenceGeometryHash) - { - const auto writtenHash = hashGeometry(m_currentCpuGeom.get()); - if (writtenHash != m_referenceGeometryHash) - failExit("Geometry hash mismatch for %s. Current=%s Reference=%s ReferenceFile=%s", m_caseName.c_str(), geometryHashToHex(writtenHash).c_str(), geometryHashToHex(m_referenceGeometryHash).c_str(), m_caseGeometryHashReferencePath.empty() ? "" : m_caseGeometryHashReferencePath.string().c_str()); - } - uint64_t diffCodeUnitCount = 0u; uint32_t maxDiffCodeUnitValue = 0u; if (!compareImages(m_loadedScreenshot.get(), m_writtenScreenshot.get(), diffCodeUnitCount, maxDiffCodeUnitValue)) diff --git a/12_MeshLoaders/README.md b/12_MeshLoaders/README.md index 5b47c17a5..c40c9dd9b 100644 --- a/12_MeshLoaders/README.md +++ b/12_MeshLoaders/README.md @@ -6,7 +6,6 @@ Example for loading and writing `OBJ`, `PLY` and `STL` meshes. - Default input list: `inputs.json` - Default mode: `batch` - Default tuning: `heuristic` -- Loader content hashes: enabled by default - Output meshes: `saved/` - Output screenshots: `screenshots/` @@ -18,8 +17,6 @@ Example for loading and writing `OBJ`, `PLY` and `STL` meshes. - Opens file dialog and loads one model. - `ci` - Runs strict pass/fail validation per case. -- `hash-test` - - Headless `IPreHashed` integrity check per geometry (invalid before recompute, valid after recompute). ## Row view concept - `row_view` means one scene containing all cases from the test list. @@ -32,10 +29,6 @@ Example for loading and writing `OBJ`, `PLY` and `STL` meshes. - run with `--interactive` - Validate load/write correctness: - run with `--ci` -- Refresh geometry references: - - run with `--update-references` (usually with `--ci`) -- Validate content hash consistency only (no write/screenshot roundtrip): - - run with `--hash-test` ## Optional benchmark datasets via CMake - Use this when you want larger/public inputs downloaded automatically. @@ -44,7 +37,6 @@ Example for loading and writing `OBJ`, `PLY` and `STL` meshes. - Configure options: - `NBL_MESHLOADERS_ENABLE_BENCHMARK_DATASETS=ON` - `NBL_MESHLOADERS_DEFAULT_START_WITH_BENCHMARK_TESTLIST=ON|OFF` (default: `OFF`) - - `NBL_MESHLOADERS_ENABLE_HASH_CTESTS=ON|OFF` (default: `OFF`) - `NBL_MESHLOADERS_BENCHMARK_DATASET_DIR=` (optional, default: build dir) - `NBL_MESHLOADERS_BENCHMARK_DATASET_REPO=` (optional, default: public repo above) - `NBL_MESHLOADERS_BENCHMARK_PAYLOAD_RELATIVE_PATH=` (optional, default: `inputs_benchmark.json`) @@ -62,23 +54,18 @@ Example for loading and writing `OBJ`, `PLY` and `STL` meshes. - Run benchmark CI directly via `ctest`: - `ctest --output-on-failure -C Debug -R NBL_MESHLOADERS_CI_BENCHMARK` - runs both benchmark CI modes: `heuristic` and `hybrid` - - benchmark CTest uses `--update-references` for payload-driven case names - Run default CI directly via `ctest` (no benchmark datasets enabled): - `ctest --output-on-failure -C Debug -R ^NBL_MESHLOADERS_CI$` - uses default `inputs.json` (3 inputs) -- Run hash consistency tests directly via `ctest`: - - `ctest --output-on-failure -C Debug -R NBL_MESHLOADERS_HASH_TEST` - - requires `NBL_MESHLOADERS_ENABLE_HASH_CTESTS=ON` at configure time - - runs both tuning modes: `heuristic` and `hybrid` - - if benchmark datasets are enabled, hash tests use benchmark payload test list +- Hash parity tests now live in Nabla tool: + - `ctest --output-on-failure -C Debug -R NBL_HCP` + - tool: `tools/hcp` (headless, dummy-blob based parity checks) ## CLI - `--ci` - strict validation run - `--interactive` - file-dialog run -- `--hash-test` - - headless load + prehash consistency check, then exit - `--testlist ` - custom JSON list - `--savegeometry` @@ -91,12 +78,8 @@ Example for loading and writing `OBJ`, `PLY` and `STL` meshes. - duplicate last row-view case - `--loader-perf-log ` - redirect loader diagnostics -- `--runtime-tuning ` +- `--runtime-tuning ` - IO runtime tuning mode -- `--loader-content-hashes` - - compatibility switch; already enabled by default -- `--update-references` - - regenerate `references/*.geomhash` ## Controls (non-CI) - Arrow keys: move camera @@ -124,8 +107,6 @@ Rules: - relative paths resolve against JSON file directory ## What CI validates -- Per-case geometry hash: - - deterministic `BLAKE3` hash compared with `references/*.geomhash` - Per-case image consistency: - `*_loaded.png` vs `*_written.png` code-unit diff - thresholds come from `MaxImageDiffCodeUnits` and `MaxImageDiffCodeUnitValue` in `MeshLoadersApp.hpp` From 4eb131463299c77ea8f3ed0e17ede02271f89bff Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Fri, 6 Mar 2026 06:55:43 +0100 Subject: [PATCH 16/34] Refactor MeshLoaders app runtime state --- 12_MeshLoaders/MeshLoadersApp.hpp | 92 +++--- 12_MeshLoaders/MeshLoadersAppLifecycle.cpp | 313 ++++++++++++--------- 12_MeshLoaders/MeshLoadersAppLoad.cpp | 67 ++--- 12_MeshLoaders/MeshLoadersAppRuntime.cpp | 45 +-- 4 files changed, 285 insertions(+), 232 deletions(-) diff --git a/12_MeshLoaders/MeshLoadersApp.hpp b/12_MeshLoaders/MeshLoadersApp.hpp index 2a43d12a0..451cb8197 100644 --- a/12_MeshLoaders/MeshLoadersApp.hpp +++ b/12_MeshLoaders/MeshLoadersApp.hpp @@ -121,6 +121,57 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built uintmax_t inputSize = 0u; }; + struct RuntimeState + { + bool nonInteractiveTest = false; + bool rowViewEnabled = true; + bool forceRowViewForCurrentTestList = false; + bool rowViewScreenshotCaptured = false; + bool fileDialogOpen = false; + + RunMode mode = RunMode::Batch; + Phase phase = Phase::RenderOriginal; + uint32_t phaseFrameCounter = 0u; + size_t caseIndex = 0u; + core::vector cases; + std::unordered_map caseNameCounts; + bool shouldQuit = false; + }; + + struct OutputState + { + bool saveGeom = true; + std::optional specifiedGeomSavePath; + nbl::system::path saveGeomPrefixPath; + nbl::system::path screenshotPrefixPath; + nbl::system::path rowViewScreenshotPath; + nbl::system::path testListPath; + std::optional loaderPerfLogPath; + std::optional rowAddPath; + uint32_t rowDuplicateCount = 0u; + + nbl::system::path writtenPath; + nbl::system::path loadedScreenshotPath; + nbl::system::path writtenScreenshotPath; + }; + + struct RenderState + { + smart_refctd_ptr renderer; + smart_refctd_ptr semaphore; + uint64_t realFrameIx = 0u; + std::array, 3u> cmdBufs; + + core::smart_refctd_ptr currentCpuGeom; + core::smart_refctd_ptr loadedScreenshot; + core::smart_refctd_ptr writtenScreenshot; + }; + + struct RowViewState + { + std::unordered_map cache; + }; + public: MeshLoadersApp(const path& localInputCWD, const path& localOutputCWD, const path& sharedInputCWD, const path& sharedOutputCWD); @@ -203,10 +254,10 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built constexpr static inline uint64_t MaxImageDiffCodeUnits = 16u; constexpr static inline uint32_t MaxImageDiffCodeUnitValue = 1u; - smart_refctd_ptr m_renderer; - smart_refctd_ptr m_semaphore; - uint64_t m_realFrameIx = 0; - std::array, MaxFramesInFlight> m_cmdBufs; + RenderState m_render; + RuntimeState m_runtime; + OutputState m_output; + RowViewState m_rowView; InputSystem::ChannelReader mouse; InputSystem::ChannelReader keyboard; @@ -226,43 +277,10 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built std::vector m_obbInstances; #endif - bool m_nonInteractiveTest = false; - bool m_rowViewEnabled = true; - bool m_forceRowViewForCurrentTestList = false; - bool m_rowViewScreenshotCaptured = false; - bool m_fileDialogOpen = false; - - bool m_saveGeom = true; - std::optional m_specifiedGeomSavePath; - nbl::system::path m_saveGeomPrefixPath; - nbl::system::path m_screenshotPrefixPath; - nbl::system::path m_rowViewScreenshotPath; - nbl::system::path m_testListPath; - std::optional m_loaderPerfLogPath; - std::optional m_rowAddPath; - uint32_t m_rowDuplicateCount = 0u; smart_refctd_ptr m_assetLoadLogger; smart_refctd_ptr m_loaderPerfLogger; asset::SFileIOPolicy::SRuntimeTuning::Mode m_runtimeTuningMode = asset::SFileIOPolicy::SRuntimeTuning::Mode::Heuristic; - RunMode m_runMode = RunMode::Batch; - Phase m_phase = Phase::RenderOriginal; - uint32_t m_phaseFrameCounter = 0u; - size_t m_caseIndex = 0u; - core::vector m_cases; - std::unordered_map m_caseNameCounts; - std::unordered_map m_rowViewCache; - bool m_shouldQuit = false; - - nbl::system::path m_writtenPath; - nbl::system::path m_loadedScreenshotPath; - nbl::system::path m_writtenScreenshotPath; - - core::smart_refctd_ptr m_currentCpuGeom; - - core::smart_refctd_ptr m_loadedScreenshot; - core::smart_refctd_ptr m_writtenScreenshot; - std::optional m_referenceCamera; }; diff --git a/12_MeshLoaders/MeshLoadersAppLifecycle.cpp b/12_MeshLoaders/MeshLoadersAppLifecycle.cpp index bde919107..53fd9a16e 100644 --- a/12_MeshLoaders/MeshLoadersAppLifecycle.cpp +++ b/12_MeshLoaders/MeshLoadersAppLifecycle.cpp @@ -8,6 +8,7 @@ #include "MeshLoadersApp.hpp" #include +#include #include #include #include @@ -20,6 +21,75 @@ #include "nbl/system/CFileLogger.h" +namespace +{ + +void setupMeshLoadersArgumentParser(argparse::ArgumentParser& parser) +{ + parser.add_argument("--savegeometry") + .help("Save the mesh on exit or reload") + .flag(); + + parser.add_argument("--savepath") + .nargs(1) + .help("Specify the file to which the mesh will be saved"); + parser.add_argument("--ci") + .help("Run in CI mode: load test list, write .ply, capture screenshots, compare data, and exit.") + .flag(); + parser.add_argument("--interactive") + .help("Use file dialog to select a single model.") + .flag(); + parser.add_argument("--testlist") + .nargs(1) + .help("JSON file with test cases. Relative paths are resolved against local input CWD."); + parser.add_argument("--row-add") + .nargs(1) + .help("Add a model path to row view on startup without using a dialog."); + parser.add_argument("--row-duplicate") + .nargs(1) + .help("Duplicate the last case N times on startup."); + parser.add_argument("--loader-perf-log") + .nargs(1) + .help("Write loader diagnostics to a file instead of stdout."); + parser.add_argument("--runtime-tuning") + .nargs(1) + .help("Runtime tuning mode for loaders: sequential|heuristic|hybrid. Default: heuristic."); +} + +std::optional parseUInt32Argument(const std::string_view value) +{ + uint32_t parsed = 0u; + const auto parseResult = std::from_chars(value.data(), value.data() + value.size(), parsed, 10); + if (parseResult.ec != std::errc() || parseResult.ptr != value.data() + value.size()) + return std::nullopt; + return parsed; +} + +bool parseRuntimeTuningMode(const std::string_view modeRaw, asset::SFileIOPolicy::SRuntimeTuning::Mode& outMode) +{ + std::string mode(modeRaw); + std::transform(mode.begin(), mode.end(), mode.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + + if (mode == "sequential" || mode == "none") + { + outMode = asset::SFileIOPolicy::SRuntimeTuning::Mode::Sequential; + return true; + } + if (mode == "heuristic") + { + outMode = asset::SFileIOPolicy::SRuntimeTuning::Mode::Heuristic; + return true; + } + if (mode == "hybrid") + { + outMode = asset::SFileIOPolicy::SRuntimeTuning::Mode::Hybrid; + return true; + } + return false; +} + +} + system::path MeshLoadersApp::resolveRuntimeCWD(const system::path& preferred) { if (preferred.empty() || preferred == path("/") || preferred == path("\\")) @@ -128,41 +198,14 @@ MeshLoadersApp::MeshLoadersApp( bool MeshLoadersApp::parseCommandLineOptions(const system::path& effectiveInputCWD, const system::path& effectiveOutputCWD, const system::path& defaultBenchmarkTestListPath) { - m_runMode = RunMode::Batch; - m_saveGeomPrefixPath = effectiveOutputCWD / "saved"; - m_screenshotPrefixPath = effectiveOutputCWD / "screenshots"; - m_testListPath = effectiveInputCWD / "inputs.json"; - m_forceRowViewForCurrentTestList = false; + m_runtime.mode = RunMode::Batch; + m_output.saveGeomPrefixPath = effectiveOutputCWD / "saved"; + m_output.screenshotPrefixPath = effectiveOutputCWD / "screenshots"; + m_output.testListPath = effectiveInputCWD / "inputs.json"; + m_runtime.forceRowViewForCurrentTestList = false; argparse::ArgumentParser parser("12_meshloaders"); - parser.add_argument("--savegeometry") - .help("Save the mesh on exit or reload") - .flag(); - - parser.add_argument("--savepath") - .nargs(1) - .help("Specify the file to which the mesh will be saved"); - parser.add_argument("--ci") - .help("Run in CI mode: load test list, write .ply, capture screenshots, compare data, and exit.") - .flag(); - parser.add_argument("--interactive") - .help("Use file dialog to select a single model.") - .flag(); - parser.add_argument("--testlist") - .nargs(1) - .help("JSON file with test cases. Relative paths are resolved against local input CWD."); - parser.add_argument("--row-add") - .nargs(1) - .help("Add a model path to row view on startup without using a dialog."); - parser.add_argument("--row-duplicate") - .nargs(1) - .help("Duplicate the last case N times on startup."); - parser.add_argument("--loader-perf-log") - .nargs(1) - .help("Write loader diagnostics to a file instead of stdout."); - parser.add_argument("--runtime-tuning") - .nargs(1) - .help("Runtime tuning mode for loaders: sequential|heuristic|hybrid. Default: heuristic."); + setupMeshLoadersArgumentParser(parser); try { @@ -174,11 +217,11 @@ bool MeshLoadersApp::parseCommandLineOptions(const system::path& effectiveInputC } if (parser["--savegeometry"] == true) - m_saveGeom = true; + m_output.saveGeom = true; if (parser["--interactive"] == true) - m_runMode = RunMode::Interactive; + m_runtime.mode = RunMode::Interactive; if (parser["--ci"] == true) - m_runMode = RunMode::CI; + m_runtime.mode = RunMode::CI; const bool hasExplicitTestListArg = parser.present("--testlist").has_value(); if (parser.present("--savepath")) @@ -188,7 +231,7 @@ bool MeshLoadersApp::parseCommandLineOptions(const system::path& effectiveInputC return logFail("Invalid path has been specified in --savepath argument"); if (!std::filesystem::exists(tmp.parent_path())) return logFail("Path specified in --savepath argument doesn't exist"); - m_specifiedGeomSavePath.emplace(std::move(tmp.generic_string())); + m_output.specifiedGeomSavePath.emplace(std::move(tmp.generic_string())); } if (hasExplicitTestListArg) @@ -198,16 +241,16 @@ bool MeshLoadersApp::parseCommandLineOptions(const system::path& effectiveInputC return logFail("Invalid path has been specified in --testlist argument"); if (tmp.is_relative()) tmp = effectiveInputCWD / tmp; - m_testListPath = tmp; + m_output.testListPath = tmp; } - else if (m_runMode == RunMode::Batch && !defaultBenchmarkTestListPath.empty()) + else if (m_runtime.mode == RunMode::Batch && !defaultBenchmarkTestListPath.empty()) { std::error_code benchmarkPathEc; if (std::filesystem::exists(defaultBenchmarkTestListPath, benchmarkPathEc) && !benchmarkPathEc) { - m_testListPath = defaultBenchmarkTestListPath; - m_forceRowViewForCurrentTestList = true; - m_logger->log("Using benchmark test list for default batch startup: %s", ILogger::ELL_INFO, m_testListPath.string().c_str()); + m_output.testListPath = defaultBenchmarkTestListPath; + m_runtime.forceRowViewForCurrentTestList = true; + m_logger->log("Using benchmark test list for default batch startup: %s", ILogger::ELL_INFO, m_output.testListPath.string().c_str()); } } @@ -216,19 +259,15 @@ bool MeshLoadersApp::parseCommandLineOptions(const system::path& effectiveInputC auto tmp = path(parser.get("--row-add")); if (tmp.is_relative()) tmp = effectiveInputCWD / tmp; - m_rowAddPath = tmp; + m_output.rowAddPath = tmp; } if (parser.present("--row-duplicate")) { auto countStr = parser.get("--row-duplicate"); - try - { - m_rowDuplicateCount = static_cast(std::stoul(countStr)); - } - catch (const std::exception&) - { + const auto parsedCount = parseUInt32Argument(countStr); + if (!parsedCount.has_value()) return logFail("Invalid --row-duplicate value."); - } + m_output.rowDuplicateCount = *parsedCount; } if (parser.present("--loader-perf-log")) { @@ -237,19 +276,12 @@ bool MeshLoadersApp::parseCommandLineOptions(const system::path& effectiveInputC return logFail("Invalid --loader-perf-log value."); if (tmp.is_relative()) tmp = effectiveOutputCWD / tmp; - m_loaderPerfLogPath = tmp; + m_output.loaderPerfLogPath = tmp; } if (parser.present("--runtime-tuning")) { auto mode = parser.get("--runtime-tuning"); - std::transform(mode.begin(), mode.end(), mode.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); - if (mode == "sequential" || mode == "none") - m_runtimeTuningMode = asset::SFileIOPolicy::SRuntimeTuning::Mode::Sequential; - else if (mode == "heuristic") - m_runtimeTuningMode = asset::SFileIOPolicy::SRuntimeTuning::Mode::Heuristic; - else if (mode == "hybrid") - m_runtimeTuningMode = asset::SFileIOPolicy::SRuntimeTuning::Mode::Hybrid; - else + if (!parseRuntimeTuningMode(mode, m_runtimeTuningMode)) return logFail("Invalid --runtime-tuning value. Expected: sequential|heuristic|hybrid."); } @@ -276,19 +308,19 @@ bool MeshLoadersApp::onAppInitialized(smart_refctd_ptr&& system) if (!parseCommandLineOptions(effectiveInputCWD, effectiveOutputCWD, defaultBenchmarkTestListPath)) return false; - if (m_saveGeom) - std::filesystem::create_directories(m_saveGeomPrefixPath); - std::filesystem::create_directories(m_screenshotPrefixPath); + if (m_output.saveGeom) + std::filesystem::create_directories(m_output.saveGeomPrefixPath); + std::filesystem::create_directories(m_output.screenshotPrefixPath); m_assetLoadLogger = m_logger; - if (m_loaderPerfLogPath) + if (m_output.loaderPerfLogPath) { - if (!initLoaderPerfLogger(*m_loaderPerfLogPath)) + if (!initLoaderPerfLogger(*m_output.loaderPerfLogPath)) return false; - m_logger->log("Loader diagnostics will be written to %s", ILogger::ELL_INFO, m_loaderPerfLogPath->string().c_str()); + m_logger->log("Loader diagnostics will be written to %s", ILogger::ELL_INFO, m_output.loaderPerfLogPath->string().c_str()); } - m_semaphore = m_device->createSemaphore(m_realFrameIx); - if (!m_semaphore) + m_render.semaphore = m_device->createSemaphore(m_render.realFrameIx); + if (!m_render.semaphore) return logFail("Failed to Create a Semaphore!"); auto pool = m_device->createCommandPool(getGraphicsQueue()->getFamilyIndex(), IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT); @@ -296,13 +328,13 @@ bool MeshLoadersApp::onAppInitialized(smart_refctd_ptr&& system) { if (!pool) return logFail("Couldn't create Command Pool!"); - if (!pool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, { m_cmdBufs.data() + i,1 })) + if (!pool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, { m_render.cmdBufs.data() + i,1 })) return logFail("Couldn't create Command Buffer!"); } auto scRes = static_cast(m_surface->getSwapchainResources()); - m_renderer = CSimpleDebugRenderer::create(m_assetMgr.get(), scRes->getRenderpass(), 0, {}); - if (!m_renderer) + m_render.renderer = CSimpleDebugRenderer::create(m_assetMgr.get(), scRes->getRenderpass(), 0, {}); + if (!m_render.renderer) return logFail("Failed to create renderer!"); #ifdef NBL_BUILD_DEBUG_DRAW @@ -324,24 +356,24 @@ bool MeshLoadersApp::onAppInitialized(smart_refctd_ptr&& system) if (isRowViewActive()) { - m_nonInteractiveTest = false; + m_runtime.nonInteractiveTest = false; if (!loadRowView(RowViewReloadMode::Full)) return false; - if (m_rowAddPath) - if (!addRowViewCaseFromPath(*m_rowAddPath)) + if (m_output.rowAddPath) + if (!addRowViewCaseFromPath(*m_output.rowAddPath)) return false; - if (m_rowDuplicateCount > 0u && !m_cases.empty()) + if (m_output.rowDuplicateCount > 0u && !m_runtime.cases.empty()) { - const auto lastPath = m_cases.back().path; - for (uint32_t i = 0u; i < m_rowDuplicateCount; ++i) + const auto lastPath = m_runtime.cases.back().path; + for (uint32_t i = 0u; i < m_output.rowDuplicateCount; ++i) if (!addRowViewCaseFromPath(lastPath)) return false; } } else { - if (m_runMode != RunMode::Interactive) - m_nonInteractiveTest = true; + if (m_runtime.mode != RunMode::Interactive) + m_runtime.nonInteractiveTest = true; if (!startCase(0u)) return false; } @@ -357,9 +389,9 @@ IQueue::SSubmitInfo::SSemaphoreInfo MeshLoadersApp::renderFrame(const std::chron m_inputSystem->getDefaultMouse(&mouse); m_inputSystem->getDefaultKeyboard(&keyboard); - const auto resourceIx = m_realFrameIx % MaxFramesInFlight; + const auto resourceIx = m_render.realFrameIx % MaxFramesInFlight; - auto* const cb = m_cmdBufs.data()[resourceIx].get(); + auto* const cb = m_render.cmdBufs.data()[resourceIx].get(); cb->reset(IGPUCommandBuffer::RESET_FLAGS::RELEASE_RESOURCES_BIT); cb->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); // clear to black for both things @@ -395,7 +427,7 @@ IQueue::SSubmitInfo::SSemaphoreInfo MeshLoadersApp::renderFrame(const std::chron cb->setScissor(0u,1u,¤tRenderArea); } // late latch input - if (!m_nonInteractiveTest) + if (!m_runtime.nonInteractiveTest) { struct SPendingInputActions { @@ -451,11 +483,11 @@ IQueue::SSubmitInfo::SSemaphoreInfo MeshLoadersApp::renderFrame(const std::chron const auto& viewMatrix = camera.getViewMatrix(); const auto& viewProjMatrix = camera.getConcatenatedMatrix(); { - m_renderer->render(cb,CSimpleDebugRenderer::SViewParams(viewMatrix,viewProjMatrix)); + m_render.renderer->render(cb,CSimpleDebugRenderer::SViewParams(viewMatrix,viewProjMatrix)); } #ifdef NBL_BUILD_DEBUG_DRAW { - const ISemaphore::SWaitInfo drawFinished = { .semaphore = m_semaphore.get(),.value = m_realFrameIx + 1u }; + const ISemaphore::SWaitInfo drawFinished = { .semaphore = m_render.semaphore.get(),.value = m_render.realFrameIx + 1u }; ext::debug_draw::DrawAABB::DrawParameters drawParams; drawParams.commandBuffer = cb; drawParams.cameraMat = viewProjMatrix; @@ -468,8 +500,8 @@ IQueue::SSubmitInfo::SSemaphoreInfo MeshLoadersApp::renderFrame(const std::chron IQueue::SSubmitInfo::SSemaphoreInfo retval = { - .semaphore = m_semaphore.get(), - .value = ++m_realFrameIx, + .semaphore = m_render.semaphore.get(), + .value = ++m_render.realFrameIx, .stageMask = PIPELINE_STAGE_FLAGS::ALL_GRAPHICS_BITS }; const IQueue::SSubmitInfo::SCommandBufferInfo commandBuffers[] = @@ -495,7 +527,7 @@ IQueue::SSubmitInfo::SSemaphoreInfo MeshLoadersApp::renderFrame(const std::chron if (getGraphicsQueue()->submit(infos) != IQueue::RESULT::SUCCESS) { retval.semaphore = nullptr; // so that we don't wait on semaphore that will never signal - m_realFrameIx--; + m_render.realFrameIx--; } std::string caption = "[Nabla Engine] Mesh Loaders"; @@ -505,11 +537,11 @@ IQueue::SSubmitInfo::SSemaphoreInfo MeshLoadersApp::renderFrame(const std::chron caption += "]"; m_window->setCaption(caption); } - if (isRowViewActive() && !m_rowViewScreenshotCaptured && m_realFrameIx >= RowViewFramesBeforeCapture) + if (isRowViewActive() && !m_runtime.rowViewScreenshotCaptured && m_render.realFrameIx >= RowViewFramesBeforeCapture) { - if (!captureScreenshot(m_rowViewScreenshotPath, m_loadedScreenshot)) + if (!captureScreenshot(m_output.rowViewScreenshotPath, m_render.loadedScreenshot)) failExit("Failed to capture row view screenshot."); - m_rowViewScreenshotCaptured = true; + m_runtime.rowViewScreenshotCaptured = true; } advanceCase(); return retval; @@ -522,7 +554,7 @@ bool MeshLoadersApp::onAppTerminated() bool MeshLoadersApp::shouldKeepRunning() const { - return !m_shouldQuit; + return !m_runtime.shouldQuit; } const video::IGPURenderpass::SCreationParams::SSubpassDependency* MeshLoadersApp::getDefaultSubpassDependencies() const @@ -577,22 +609,22 @@ const video::IGPURenderpass::SCreationParams::SSubpassDependency* MeshLoadersApp bool MeshLoadersApp::initTestCases() { - m_cases.clear(); - m_caseNameCounts.clear(); - if (m_runMode == RunMode::Interactive) + m_runtime.cases.clear(); + m_runtime.caseNameCounts.clear(); + if (m_runtime.mode == RunMode::Interactive) { system::path picked; if (!pickModelPath(picked)) return logFail("No file selected."); - m_cases.push_back({ makeUniqueCaseName(picked), picked }); + m_runtime.cases.push_back({ makeUniqueCaseName(picked), picked }); return true; } - return loadTestList(m_testListPath); + return loadTestList(m_output.testListPath); } bool MeshLoadersApp::pickModelPath(system::path& outPath) { - if (m_fileDialogOpen) + if (m_runtime.fileDialogOpen) { if (m_logger) m_logger->log("File dialog is already open. Ignoring request.", ILogger::ELL_WARNING); @@ -605,8 +637,8 @@ bool MeshLoadersApp::pickModelPath(system::path& outPath) ~DialogGuard() { flag = false; } }; - m_fileDialogOpen = true; - DialogGuard guard{m_fileDialogOpen}; + m_runtime.fileDialogOpen = true; + DialogGuard guard{m_runtime.fileDialogOpen}; pfd::open_file file( "Choose a supported Model File", @@ -631,7 +663,7 @@ bool MeshLoadersApp::loadTestList(const system::path& jsonPath) { if (!std::filesystem::exists(jsonPath)) return logFail("Missing test list: %s", jsonPath.string().c_str()); - m_rowViewEnabled = true; + m_runtime.rowViewEnabled = true; std::ifstream stream(jsonPath); if (!stream.is_open()) @@ -650,16 +682,16 @@ bool MeshLoadersApp::loadTestList(const system::path& jsonPath) if (!doc.contains("cases") || !doc["cases"].is_array()) return logFail("Test list JSON missing \"cases\" array."); - m_caseNameCounts.clear(); + m_runtime.caseNameCounts.clear(); if (doc.contains("row_view")) { if (!doc["row_view"].is_boolean()) return logFail("\"row_view\" must be a boolean."); - m_rowViewEnabled = doc["row_view"].get(); + m_runtime.rowViewEnabled = doc["row_view"].get(); } - if (m_forceRowViewForCurrentTestList && m_runMode == RunMode::Batch) - m_rowViewEnabled = true; + if (m_runtime.forceRowViewForCurrentTestList && m_runtime.mode == RunMode::Batch) + m_runtime.rowViewEnabled = true; const auto baseDir = jsonPath.parent_path(); for (const auto& entry : doc["cases"]) @@ -685,10 +717,10 @@ bool MeshLoadersApp::loadTestList(const system::path& jsonPath) if (!std::filesystem::exists(path)) return logFail("Missing test input: %s", path.string().c_str()); - m_cases.push_back({ makeUniqueCaseName(path), path }); + m_runtime.cases.push_back({ makeUniqueCaseName(path), path }); } - if (m_cases.empty()) + if (m_runtime.cases.empty()) return logFail("No test cases in test list."); return true; @@ -696,7 +728,7 @@ bool MeshLoadersApp::loadTestList(const system::path& jsonPath) bool MeshLoadersApp::isRowViewActive() const { - return m_rowViewEnabled && m_runMode != RunMode::CI && m_runMode != RunMode::Interactive; + return m_runtime.rowViewEnabled && m_runtime.mode != RunMode::CI && m_runtime.mode != RunMode::Interactive; } std::string MeshLoadersApp::normalizeExtension(const system::path& path) @@ -719,8 +751,8 @@ bool MeshLoadersApp::isWriteExtensionSupported(const std::string& ext) const system::path MeshLoadersApp::resolveSavePath(const system::path& modelPath) const { - if (m_specifiedGeomSavePath) - return path(*m_specifiedGeomSavePath); + if (m_output.specifiedGeomSavePath) + return path(*m_output.specifiedGeomSavePath); const auto stem = modelPath.stem().string(); auto ext = normalizeExtension(modelPath); if (ext.empty()) @@ -731,26 +763,26 @@ system::path MeshLoadersApp::resolveSavePath(const system::path& modelPath) cons m_logger->log("No writer for %s, writing .ply instead.", ILogger::ELL_WARNING, ext.c_str()); ext = ".ply"; } - return m_saveGeomPrefixPath / (stem + "_written" + ext); + return m_output.saveGeomPrefixPath / (stem + "_written" + ext); } bool MeshLoadersApp::startCase(const size_t index) { - if (index >= m_cases.size()) + if (index >= m_runtime.cases.size()) return false; - m_caseIndex = index; - m_phase = Phase::RenderOriginal; - m_phaseFrameCounter = 0u; - m_loadedScreenshot = nullptr; - m_writtenScreenshot = nullptr; + m_runtime.caseIndex = index; + m_runtime.phase = Phase::RenderOriginal; + m_runtime.phaseFrameCounter = 0u; + m_render.loadedScreenshot = nullptr; + m_render.writtenScreenshot = nullptr; m_referenceCamera.reset(); - const auto& testCase = m_cases[m_caseIndex]; + const auto& testCase = m_runtime.cases[m_runtime.caseIndex]; m_caseName = testCase.name.empty() ? testCase.path.stem().string() : testCase.name; - m_writtenPath = resolveSavePath(testCase.path); - m_loadedScreenshotPath = m_screenshotPrefixPath / ("meshloaders_" + m_caseName + "_loaded.png"); - m_writtenScreenshotPath = m_screenshotPrefixPath / ("meshloaders_" + m_caseName + "_written.png"); + m_output.writtenPath = resolveSavePath(testCase.path); + m_output.loadedScreenshotPath = m_output.screenshotPrefixPath / ("meshloaders_" + m_caseName + "_loaded.png"); + m_output.writtenScreenshotPath = m_output.screenshotPrefixPath / ("meshloaders_" + m_caseName + "_written.png"); if (!loadModel(testCase.path, true, true)) return false; @@ -760,15 +792,15 @@ bool MeshLoadersApp::startCase(const size_t index) bool MeshLoadersApp::advanceToNextCase() { - const auto nextIndex = m_caseIndex + 1u; - if (nextIndex >= m_cases.size()) + const auto nextIndex = m_runtime.caseIndex + 1u; + if (nextIndex >= m_runtime.cases.size()) { - m_shouldQuit = true; + m_runtime.shouldQuit = true; return false; } if (!startCase(nextIndex)) { - m_shouldQuit = true; + m_runtime.shouldQuit = true; return false; } return true; @@ -781,10 +813,10 @@ void MeshLoadersApp::reloadInteractive() failExit("No file selected."); if (!loadModel(picked, true, true)) failExit("Failed to load asset %s.", picked.string().c_str()); - if (m_currentCpuGeom && m_saveGeom) + if (m_render.currentCpuGeom && m_output.saveGeom) { const auto savePath = resolveSavePath(picked); - if (!writeGeometry(m_currentCpuGeom, savePath.string())) + if (!writeGeometry(m_render.currentCpuGeom, savePath.string())) failExit("Geometry write failed."); } } @@ -801,24 +833,24 @@ bool MeshLoadersApp::addRowViewCaseFromPath(const system::path& picked) { if (picked.empty()) return false; - m_cases.push_back({ makeUniqueCaseName(picked), picked }); - m_shouldQuit = false; + m_runtime.cases.push_back({ makeUniqueCaseName(picked), picked }); + m_runtime.shouldQuit = false; return loadRowView(RowViewReloadMode::Incremental); } bool MeshLoadersApp::reloadFromTestList() { - m_cases.clear(); - if (!loadTestList(m_testListPath)) + m_runtime.cases.clear(); + if (!loadTestList(m_output.testListPath)) return false; - m_shouldQuit = false; - m_rowViewScreenshotCaptured = false; + m_runtime.shouldQuit = false; + m_runtime.rowViewScreenshotCaptured = false; if (isRowViewActive()) { - m_nonInteractiveTest = false; + m_runtime.nonInteractiveTest = false; return loadRowView(RowViewReloadMode::Full); } - m_nonInteractiveTest = (m_runMode != RunMode::Interactive); + m_runtime.nonInteractiveTest = (m_runtime.mode != RunMode::Interactive); return startCase(0u); } @@ -826,20 +858,21 @@ void MeshLoadersApp::resetRowViewScene() { if (!isRowViewActive()) return; - m_cases.clear(); - m_rowViewCache.clear(); - m_renderer->m_instances.clear(); - m_renderer->clearGeometries({ .semaphore = m_semaphore.get(),.value = m_realFrameIx }); + m_runtime.cases.clear(); + m_rowView.cache.clear(); + m_render.renderer->m_instances.clear(); + m_render.renderer->clearGeometries({ .semaphore = m_render.semaphore.get(),.value = m_render.realFrameIx }); #ifdef NBL_BUILD_DEBUG_DRAW m_aabbInstances.clear(); m_obbInstances.clear(); #endif m_modelPath = "Row view (empty)"; - m_rowViewScreenshotCaptured = false; - m_shouldQuit = false; - m_nonInteractiveTest = false; + m_runtime.rowViewScreenshotCaptured = false; + m_runtime.shouldQuit = false; + m_runtime.nonInteractiveTest = false; m_logger->log("Row view reset to empty. Press A to add a model.", ILogger::ELL_INFO); } + diff --git a/12_MeshLoaders/MeshLoadersAppLoad.cpp b/12_MeshLoaders/MeshLoadersAppLoad.cpp index 3c545e08f..dd901f65f 100644 --- a/12_MeshLoaders/MeshLoadersAppLoad.cpp +++ b/12_MeshLoaders/MeshLoadersAppLoad.cpp @@ -24,8 +24,8 @@ bool MeshLoadersApp::loadModel(const system::path& modelPath, bool updateCamera, m_modelPath = modelPath.string(); // free up - m_renderer->m_instances.clear(); - m_renderer->clearGeometries({ .semaphore = m_semaphore.get(),.value = m_realFrameIx }); + m_render.renderer->m_instances.clear(); + m_render.renderer->clearGeometries({ .semaphore = m_render.semaphore.get(),.value = m_render.realFrameIx }); m_assetMgr->clearAllAssetCache(); //! load the geometry @@ -63,7 +63,7 @@ bool MeshLoadersApp::loadModel(const system::path& modelPath, bool updateCamera, outerMs, nonLoaderMs); - m_currentCpuGeom = geometries[0]; + m_render.currentCpuGeom = geometries[0]; using aabb_t = hlsl::shapes::AABB<3, double>; auto printAABB = [&](const aabb_t& aabb, const char* extraMsg = "")->void @@ -199,11 +199,11 @@ bool MeshLoadersApp::loadModel(const system::path& modelPath, bool updateCamera, } printAABB(bound, "Total"); - if (!m_renderer->addGeometries({ &converted.front().get(),converted.size() })) + if (!m_render.renderer->addGeometries({ &converted.front().get(),converted.size() })) failExit("Failed to add geometries to renderer."); if (m_logger) { - const auto& gpuGeos = m_renderer->getGeometries(); + const auto& gpuGeos = m_render.renderer->getGeometries(); for (size_t geoIx = 0u; geoIx < gpuGeos.size(); ++geoIx) { const auto& gpuGeo = gpuGeos[geoIx]; @@ -219,8 +219,8 @@ bool MeshLoadersApp::loadModel(const system::path& modelPath, bool updateCamera, } auto worlTformsIt = worldTforms.begin(); - for (const auto& geo : m_renderer->getGeometries()) - m_renderer->m_instances.push_back({ + for (const auto& geo : m_render.renderer->getGeometries()) + m_render.renderer->m_instances.push_back({ .world = *(worlTformsIt++), .packedGeo = &geo }); @@ -242,43 +242,43 @@ bool MeshLoadersApp::loadModel(const system::path& modelPath, bool updateCamera, bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) { - if (m_cases.empty()) + if (m_runtime.cases.empty()) failExit("No test cases loaded for row view."); using clock_t = std::chrono::high_resolution_clock; RowViewPerfStats stats = {}; stats.incremental = (mode == RowViewReloadMode::Incremental); - stats.cases = m_cases.size(); + stats.cases = m_runtime.cases.size(); const auto totalStart = clock_t::now(); const auto clearStart = clock_t::now(); if (mode == RowViewReloadMode::Full) { - m_renderer->m_instances.clear(); - m_renderer->clearGeometries({ .semaphore = m_semaphore.get(),.value = m_realFrameIx }); + m_render.renderer->m_instances.clear(); + m_render.renderer->clearGeometries({ .semaphore = m_render.semaphore.get(),.value = m_render.realFrameIx }); } stats.clearMs = toMs(clock_t::now() - clearStart); core::vector> geometries; core::vector> aabbs; - geometries.reserve(m_cases.size()); - aabbs.reserve(m_cases.size()); + geometries.reserve(m_runtime.cases.size()); + aabbs.reserve(m_runtime.cases.size()); core::vector> cpuToConvert; core::vector convertEntries; - m_rowViewCache.reserve(m_cases.size()); + m_rowView.cache.reserve(m_runtime.cases.size()); IAssetLoader::SAssetLoadParams params = makeLoadParams(); - for (const auto& testCase : m_cases) + for (const auto& testCase : m_runtime.cases) { const auto& path = testCase.path; if (!std::filesystem::exists(path)) failExit("Missing input: %s", path.string().c_str()); const auto cacheKey = makeCacheKey(path); - auto& entry = m_rowViewCache[cacheKey]; + auto& entry = m_rowView.cache[cacheKey]; double assetLoadMs = 0.0; bool cached = true; if (!entry.cpu) @@ -428,18 +428,18 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) stats.convertMs = toMs(clock_t::now() - convertStart); } - size_t existingCount = m_renderer->getGeometries().size(); - const bool incremental = (mode == RowViewReloadMode::Incremental) && (existingCount <= m_cases.size()); + size_t existingCount = m_render.renderer->getGeometries().size(); + const bool incremental = (mode == RowViewReloadMode::Incremental) && (existingCount <= m_runtime.cases.size()); if (!incremental && mode == RowViewReloadMode::Incremental) return loadRowView(RowViewReloadMode::Full); if (mode == RowViewReloadMode::Full) { core::vector allGeometries; - allGeometries.reserve(m_cases.size()); - for (const auto& testCase : m_cases) + allGeometries.reserve(m_runtime.cases.size()); + for (const auto& testCase : m_runtime.cases) { - const auto& entry = m_rowViewCache[makeCacheKey(testCase.path)]; + const auto& entry = m_rowView.cache[makeCacheKey(testCase.path)]; if (!entry.gpu) failExit("Missing GPU geometry for %s.", testCase.path.string().c_str()); allGeometries.push_back(entry.gpu.get()); @@ -447,27 +447,27 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) stats.addCount = allGeometries.size(); const auto addStart = clock_t::now(); if (!allGeometries.empty()) - if (!m_renderer->addGeometries({ allGeometries.data(),allGeometries.size() })) + if (!m_render.renderer->addGeometries({ allGeometries.data(),allGeometries.size() })) failExit("Failed to add geometries to renderer."); stats.addGeoMs = toMs(clock_t::now() - addStart); } else { - const size_t addCount = (existingCount < m_cases.size()) ? (m_cases.size() - existingCount) : 0u; + const size_t addCount = (existingCount < m_runtime.cases.size()) ? (m_runtime.cases.size() - existingCount) : 0u; stats.addCount = addCount; if (addCount > 0u) { core::vector newGeometries; newGeometries.reserve(addCount); - for (size_t i = existingCount; i < m_cases.size(); ++i) + for (size_t i = existingCount; i < m_runtime.cases.size(); ++i) { - const auto& entry = m_rowViewCache[makeCacheKey(m_cases[i].path)]; + const auto& entry = m_rowView.cache[makeCacheKey(m_runtime.cases[i].path)]; if (!entry.gpu) - failExit("Missing GPU geometry for %s.", m_cases[i].path.string().c_str()); + failExit("Missing GPU geometry for %s.", m_runtime.cases[i].path.string().c_str()); newGeometries.push_back(entry.gpu.get()); } const auto addStart = clock_t::now(); - if (!m_renderer->addGeometries({ newGeometries.data(),newGeometries.size() })) + if (!m_render.renderer->addGeometries({ newGeometries.data(),newGeometries.size() })) failExit("Failed to add geometries to renderer."); stats.addGeoMs = toMs(clock_t::now() - addStart); } @@ -528,7 +528,7 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) m_aabbInstances.resize(geometries.size()); if (m_drawBBMode == DBBM_OBB) m_obbInstances.resize(geometries.size()); - m_renderer->m_instances.clear(); + m_render.renderer->m_instances.clear(); for (uint32_t i = 0; i < geometries.size(); i++) { @@ -588,9 +588,9 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) printAABB(bound, "Total"); for (uint32_t i = 0; i < worldTforms.size(); i++) { - m_renderer->m_instances.push_back({ + m_render.renderer->m_instances.push_back({ .world = worldTforms[i], - .packedGeo = &m_renderer->getGeometry(i) + .packedGeo = &m_render.renderer->getGeometry(i) }); } stats.instanceMs = toMs(clock_t::now() - instanceStart); @@ -600,8 +600,8 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) stats.cameraMs = toMs(clock_t::now() - cameraStart); m_modelPath = "Row view (all meshes)"; - m_rowViewScreenshotPath = m_screenshotPrefixPath / "meshloaders_row_view.png"; - m_rowViewScreenshotCaptured = false; + m_output.rowViewScreenshotPath = m_output.screenshotPrefixPath / "meshloaders_row_view.png"; + m_runtime.rowViewScreenshotCaptured = false; stats.totalMs = toMs(clock_t::now() - totalStart); logRowViewPerf(stats); return true; @@ -764,7 +764,7 @@ IAssetLoader::SAssetLoadParams MeshLoadersApp::makeLoadParams() const { IAssetLoader::SAssetLoadParams params = {}; params.logger = getAssetLoadLogger(); - if ((m_runMode == RunMode::CI || isRowViewActive()) && !m_loaderPerfLogger) + if ((m_runtime.mode == RunMode::CI || isRowViewActive()) && !m_loaderPerfLogger) params.logger = nullptr; params.cacheFlags = IAssetLoader::ECF_DUPLICATE_TOP_LEVEL; params.ioPolicy.runtimeTuning.mode = m_runtimeTuningMode; @@ -810,3 +810,4 @@ bool MeshLoadersApp::initLoaderPerfLogger(const system::path& logPath) } + diff --git a/12_MeshLoaders/MeshLoadersAppRuntime.cpp b/12_MeshLoaders/MeshLoadersAppRuntime.cpp index 681b34144..38f476799 100644 --- a/12_MeshLoaders/MeshLoadersAppRuntime.cpp +++ b/12_MeshLoaders/MeshLoadersAppRuntime.cpp @@ -12,7 +12,7 @@ std::string MeshLoadersApp::makeUniqueCaseName(const system::path& path) auto base = path.stem().string(); if (base.empty()) base = "case"; - auto& counter = m_caseNameCounts[base]; + auto& counter = m_runtime.caseNameCounts[base]; std::string name = (counter == 0u) ? base : (base + "_" + std::to_string(counter)); ++counter; return name; @@ -197,58 +197,58 @@ bool MeshLoadersApp::compareImages( void MeshLoadersApp::advanceCase() { - if (m_runMode == RunMode::Interactive || m_cases.empty()) + if (m_runtime.mode == RunMode::Interactive || m_runtime.cases.empty()) return; if (isRowViewActive()) return; - const uint32_t frameLimit = m_runMode == RunMode::CI ? CiFramesBeforeCapture : NonCiFramesPerCase; - ++m_phaseFrameCounter; - if (m_phaseFrameCounter < frameLimit) + const uint32_t frameLimit = m_runtime.mode == RunMode::CI ? CiFramesBeforeCapture : NonCiFramesPerCase; + ++m_runtime.phaseFrameCounter; + if (m_runtime.phaseFrameCounter < frameLimit) return; - if (m_phase == Phase::RenderOriginal) + if (m_runtime.phase == Phase::RenderOriginal) { - if (!captureScreenshot(m_loadedScreenshotPath, m_loadedScreenshot)) + if (!captureScreenshot(m_output.loadedScreenshotPath, m_render.loadedScreenshot)) failExit("Failed to capture loaded screenshot."); - if (m_saveGeom) + if (m_output.saveGeom) { - if (!m_currentCpuGeom) + if (!m_render.currentCpuGeom) failExit("No geometry to write."); - if (!writeGeometry(m_currentCpuGeom, m_writtenPath.string())) + if (!writeGeometry(m_render.currentCpuGeom, m_output.writtenPath.string())) failExit("Geometry write failed."); } - if (m_runMode == RunMode::CI) + if (m_runtime.mode == RunMode::CI) { - if (!loadModel(m_writtenPath, false, false)) - failExit("Failed to load written asset %s.", m_writtenPath.string().c_str()); - if (!m_currentCpuGeom) + if (!loadModel(m_output.writtenPath, false, false)) + failExit("Failed to load written asset %s.", m_output.writtenPath.string().c_str()); + if (!m_render.currentCpuGeom) failExit("Written geometry missing."); - m_phase = Phase::RenderWritten; - m_phaseFrameCounter = 0u; + m_runtime.phase = Phase::RenderWritten; + m_runtime.phaseFrameCounter = 0u; return; } - if (m_saveGeom) + if (m_output.saveGeom) { - if (!validateWrittenAsset(m_writtenPath)) - failExit("Failed to load written asset %s.", m_writtenPath.string().c_str()); + if (!validateWrittenAsset(m_output.writtenPath)) + failExit("Failed to load written asset %s.", m_output.writtenPath.string().c_str()); } advanceToNextCase(); return; } - if (m_phase == Phase::RenderWritten) + if (m_runtime.phase == Phase::RenderWritten) { - if (!captureScreenshot(m_writtenScreenshotPath, m_writtenScreenshot)) + if (!captureScreenshot(m_output.writtenScreenshotPath, m_render.writtenScreenshot)) failExit("Failed to capture written screenshot."); uint64_t diffCodeUnitCount = 0u; uint32_t maxDiffCodeUnitValue = 0u; - if (!compareImages(m_loadedScreenshot.get(), m_writtenScreenshot.get(), diffCodeUnitCount, maxDiffCodeUnitValue)) + if (!compareImages(m_render.loadedScreenshot.get(), m_render.writtenScreenshot.get(), diffCodeUnitCount, maxDiffCodeUnitValue)) failExit("Image compare failed for %s.", m_caseName.c_str()); if (diffCodeUnitCount > MaxImageDiffCodeUnits || maxDiffCodeUnitValue > MaxImageDiffCodeUnitValue) failExit("Image diff detected for %s. CodeUnits: %llu MaxCodeUnitDiff: %u", m_caseName.c_str(), static_cast(diffCodeUnitCount), maxDiffCodeUnitValue); @@ -260,3 +260,4 @@ void MeshLoadersApp::advanceCase() } + From 906372eff5655713626a6098710fb20901f87fdf Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Fri, 6 Mar 2026 21:15:04 +0100 Subject: [PATCH 17/34] Update mesh loaders example --- 12_MeshLoaders/MeshLoadersApp.hpp | 3 ++- 12_MeshLoaders/MeshLoadersAppLifecycle.cpp | 4 ++-- 12_MeshLoaders/MeshLoadersAppLoad.cpp | 9 +++++++-- 12_MeshLoaders/MeshLoadersAppRuntime.cpp | 16 ++++------------ 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/12_MeshLoaders/MeshLoadersApp.hpp b/12_MeshLoaders/MeshLoadersApp.hpp index 451cb8197..a6d07bafe 100644 --- a/12_MeshLoaders/MeshLoadersApp.hpp +++ b/12_MeshLoaders/MeshLoadersApp.hpp @@ -162,6 +162,7 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built uint64_t realFrameIx = 0u; std::array, 3u> cmdBufs; + core::smart_refctd_ptr currentCpuAsset; core::smart_refctd_ptr currentCpuGeom; core::smart_refctd_ptr loadedScreenshot; core::smart_refctd_ptr writtenScreenshot; @@ -212,7 +213,7 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built bool loadModel(const system::path& modelPath, bool updateCamera, bool storeCamera); bool loadRowView(RowViewReloadMode mode); - bool writeGeometry(smart_refctd_ptr geometry, const std::string& savePath); + bool writeAssetRoot(smart_refctd_ptr asset, const std::string& savePath); void setupCameraFromAABB(const hlsl::shapes::AABB<3, double>& bound); diff --git a/12_MeshLoaders/MeshLoadersAppLifecycle.cpp b/12_MeshLoaders/MeshLoadersAppLifecycle.cpp index 53fd9a16e..5403d9f6d 100644 --- a/12_MeshLoaders/MeshLoadersAppLifecycle.cpp +++ b/12_MeshLoaders/MeshLoadersAppLifecycle.cpp @@ -813,10 +813,10 @@ void MeshLoadersApp::reloadInteractive() failExit("No file selected."); if (!loadModel(picked, true, true)) failExit("Failed to load asset %s.", picked.string().c_str()); - if (m_render.currentCpuGeom && m_output.saveGeom) + if (m_render.currentCpuAsset && m_output.saveGeom) { const auto savePath = resolveSavePath(picked); - if (!writeGeometry(m_render.currentCpuGeom, savePath.string())) + if (!writeAssetRoot(m_render.currentCpuAsset, savePath.string())) failExit("Geometry write failed."); } } diff --git a/12_MeshLoaders/MeshLoadersAppLoad.cpp b/12_MeshLoaders/MeshLoadersAppLoad.cpp index dd901f65f..4010a4664 100644 --- a/12_MeshLoaders/MeshLoadersAppLoad.cpp +++ b/12_MeshLoaders/MeshLoadersAppLoad.cpp @@ -44,6 +44,8 @@ bool MeshLoadersApp::loadModel(const system::path& modelPath, bool updateCamera, if (asset.getContents().empty()) failExit("Failed to load asset %s.", m_modelPath.c_str()); + m_render.currentCpuAsset = (asset.getContents().size() == 1u) ? asset.getContents()[0] : nullptr; + core::vector> geometries; const auto extractStart = clock_t::now(); if (!appendGeometriesFromBundle(asset, geometries)) @@ -607,11 +609,14 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) return true; } -bool MeshLoadersApp::writeGeometry(smart_refctd_ptr geometry, const std::string& savePath) +bool MeshLoadersApp::writeAssetRoot(smart_refctd_ptr asset, const std::string& savePath) { using clock_t = std::chrono::high_resolution_clock; const auto writeOuterStart = clock_t::now(); - IAsset* assetPtr = const_cast(static_cast(geometry.get())); + if (!asset) + return false; + + IAsset* assetPtr = const_cast(asset.get()); const auto ext = normalizeExtension(system::path(savePath)); auto flags = asset::EWF_MESH_IS_RIGHT_HANDED; if (ext != ".obj") diff --git a/12_MeshLoaders/MeshLoadersAppRuntime.cpp b/12_MeshLoaders/MeshLoadersAppRuntime.cpp index 38f476799..99c567dea 100644 --- a/12_MeshLoaders/MeshLoadersAppRuntime.cpp +++ b/12_MeshLoaders/MeshLoadersAppRuntime.cpp @@ -95,16 +95,8 @@ bool MeshLoadersApp::validateWrittenAsset(const system::path& path) return false; core::vector> geometries; - switch (asset.getAssetType()) - { - case IAsset::E_TYPE::ET_GEOMETRY: - for (const auto& item : asset.getContents()) - if (auto polyGeo = IAsset::castDown(item); polyGeo) - geometries.push_back(polyGeo); - break; - default: + if (!appendGeometriesFromBundle(asset, geometries)) return false; - } return !geometries.empty(); } @@ -214,9 +206,9 @@ void MeshLoadersApp::advanceCase() if (m_output.saveGeom) { - if (!m_render.currentCpuGeom) - failExit("No geometry to write."); - if (!writeGeometry(m_render.currentCpuGeom, m_output.writtenPath.string())) + if (!m_render.currentCpuAsset) + failExit("No single root asset to write."); + if (!writeAssetRoot(m_render.currentCpuAsset, m_output.writtenPath.string())) failExit("Geometry write failed."); } From dd7fa53e958c40a187ba78c098c7bc0e18e4cdab Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sun, 8 Mar 2026 22:05:01 +0100 Subject: [PATCH 18/34] Refine mesh loaders example flow --- 12_MeshLoaders/CMakeLists.txt | 8 + 12_MeshLoaders/MeshLoadersApp.hpp | 9 +- 12_MeshLoaders/MeshLoadersAppLifecycle.cpp | 10 + 12_MeshLoaders/MeshLoadersAppLoad.cpp | 307 ++++++++++++--------- 12_MeshLoaders/MeshLoadersAppRuntime.cpp | 14 +- 12_MeshLoaders/README.md | 2 +- 12_MeshLoaders/inputs.json | 2 +- 7 files changed, 210 insertions(+), 142 deletions(-) diff --git a/12_MeshLoaders/CMakeLists.txt b/12_MeshLoaders/CMakeLists.txt index 566bf07d1..d27f11688 100644 --- a/12_MeshLoaders/CMakeLists.txt +++ b/12_MeshLoaders/CMakeLists.txt @@ -67,7 +67,14 @@ if (NBL_MESHLOADERS_ENABLE_BENCHMARK_DATASETS) ) FetchContent_GetProperties(nbl_meshloaders_benchmark_dataset) if (NOT nbl_meshloaders_benchmark_dataset_POPULATED) + if (POLICY CMP0169) + cmake_policy(PUSH) + cmake_policy(SET CMP0169 OLD) + endif() FetchContent_Populate(nbl_meshloaders_benchmark_dataset) + if (POLICY CMP0169) + cmake_policy(POP) + endif() endif () endif () endif () @@ -102,6 +109,7 @@ if (NBL_MESHLOADERS_ENABLE_BENCHMARK_DATASETS) set(_NBL_MESHLOADERS_BENCHMARK_CI_COMMON_ARGS --ci --testlist "${NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON}" + --loader-content-hashes ) add_test(NAME NBL_MESHLOADERS_CI_BENCHMARK_HEURISTIC diff --git a/12_MeshLoaders/MeshLoadersApp.hpp b/12_MeshLoaders/MeshLoadersApp.hpp index a6d07bafe..5fadeb8ad 100644 --- a/12_MeshLoaders/MeshLoadersApp.hpp +++ b/12_MeshLoaders/MeshLoadersApp.hpp @@ -78,10 +78,9 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built struct CachedGeometryEntry { - smart_refctd_ptr cpu; - video::asset_cached_t gpu; - hlsl::shapes::AABB<3, double> aabb = hlsl::shapes::AABB<3, double>::create(); - bool hasAabb = false; + core::vector> cpu; + core::vector> gpu; + core::vector> aabbs; }; struct RowViewPerfStats @@ -281,6 +280,8 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built smart_refctd_ptr m_assetLoadLogger; smart_refctd_ptr m_loaderPerfLogger; asset::SFileIOPolicy::SRuntimeTuning::Mode m_runtimeTuningMode = asset::SFileIOPolicy::SRuntimeTuning::Mode::Heuristic; + bool m_forceLoaderContentHashes = false; + bool m_updateGeometryHashReferences = false; std::optional m_referenceCamera; }; diff --git a/12_MeshLoaders/MeshLoadersAppLifecycle.cpp b/12_MeshLoaders/MeshLoadersAppLifecycle.cpp index 5403d9f6d..8ede7df63 100644 --- a/12_MeshLoaders/MeshLoadersAppLifecycle.cpp +++ b/12_MeshLoaders/MeshLoadersAppLifecycle.cpp @@ -51,6 +51,12 @@ void setupMeshLoadersArgumentParser(argparse::ArgumentParser& parser) parser.add_argument("--loader-perf-log") .nargs(1) .help("Write loader diagnostics to a file instead of stdout."); + parser.add_argument("--loader-content-hashes") + .help("Force loaders to compute CPU buffer content hashes before returning.") + .flag(); + parser.add_argument("--update-references") + .help("Accept official benchmark/reference CLI without changing current local flow.") + .flag(); parser.add_argument("--runtime-tuning") .nargs(1) .help("Runtime tuning mode for loaders: sequential|heuristic|hybrid. Default: heuristic."); @@ -278,6 +284,10 @@ bool MeshLoadersApp::parseCommandLineOptions(const system::path& effectiveInputC tmp = effectiveOutputCWD / tmp; m_output.loaderPerfLogPath = tmp; } + if (parser["--loader-content-hashes"] == true) + m_forceLoaderContentHashes = true; + if (parser["--update-references"] == true) + m_updateGeometryHashReferences = true; if (parser.present("--runtime-tuning")) { auto mode = parser.get("--runtime-tuning"); diff --git a/12_MeshLoaders/MeshLoadersAppLoad.cpp b/12_MeshLoaders/MeshLoadersAppLoad.cpp index 4010a4664..c6ab56722 100644 --- a/12_MeshLoaders/MeshLoadersAppLoad.cpp +++ b/12_MeshLoaders/MeshLoadersAppLoad.cpp @@ -12,6 +12,96 @@ #include #include "nbl/examples/common/GeometryAABBUtilities.h" +namespace +{ +using display_aabb_t = hlsl::shapes::AABB<3, double>; +struct DisplayLayout +{ + core::vector worldTransforms = {}; + display_aabb_t bound = display_aabb_t::create(); +}; +static hlsl::float32_t3x4 makeIdentityWorld() +{ + auto tmp = hlsl::float32_t4x3( + hlsl::float32_t3(1, 0, 0), + hlsl::float32_t3(0, 1, 0), + hlsl::float32_t3(0, 0, 1), + hlsl::float32_t3(0, 0, 0)); + return hlsl::transpose(tmp); +} +static DisplayLayout buildDisplayLayout(const core::vector& aabbs, const bool arrangeInRow) +{ + DisplayLayout retval = {}; + retval.worldTransforms.reserve(aabbs.size()); + if (!arrangeInRow) + { + const auto identity = makeIdentityWorld(); + for (const auto& aabb : aabbs) + { + retval.worldTransforms.push_back(identity); + retval.bound = hlsl::shapes::util::union_(aabb, retval.bound); + } + return retval; + } + double targetExtent = 0.0; + core::vector maxDims; + maxDims.reserve(aabbs.size()); + for (const auto& aabb : aabbs) + { + const auto extent = aabb.getExtent(); + const double maxDim = std::max({extent.x, extent.y, extent.z, 0.001}); + maxDims.push_back(maxDim); + if (maxDim > targetExtent) + targetExtent = maxDim; + } + core::vector scales; + scales.reserve(aabbs.size()); + for (const auto maxDim : maxDims) + scales.push_back(targetExtent / maxDim); + double maxWidth = 0.0; + double totalWidth = 0.0; + core::vector widths; + widths.reserve(aabbs.size()); + for (size_t i = 0u; i < aabbs.size(); ++i) + { + const auto extent = aabbs[i].getExtent(); + const double width = std::max(0.001, extent.x * scales[i]); + widths.push_back(width); + totalWidth += width; + if (width > maxWidth) + maxWidth = width; + } + const double spacing = std::max(0.05 * maxWidth, 0.01); + const double totalSpan = totalWidth + spacing * double(widths.size() > 0u ? widths.size() - 1u : 0u); + double cursor = -0.5 * totalSpan; + auto tmp = hlsl::float32_t4x3( + hlsl::float32_t3(1, 0, 0), + hlsl::float32_t3(0, 1, 0), + hlsl::float32_t3(0, 0, 1), + hlsl::float32_t3(0, 0, 0)); + for (size_t i = 0u; i < aabbs.size(); ++i) + { + const auto& aabb = aabbs[i]; + const double scale = scales[i]; + const auto center = (aabb.minVx + aabb.maxVx) * 0.5; + const double width = widths[i]; + const double targetCenterX = cursor + 0.5 * width; + cursor += width + spacing; + const double tx = targetCenterX - scale * center.x; + const double ty = -scale * center.y; + const double tz = -scale * center.z; + tmp[0] = hlsl::float32_t3(static_cast(scale), 0.f, 0.f); + tmp[1] = hlsl::float32_t3(0.f, static_cast(scale), 0.f); + tmp[2] = hlsl::float32_t3(0.f, 0.f, static_cast(scale)); + tmp[3] = hlsl::float32_t3(static_cast(tx), static_cast(ty), static_cast(tz)); + const auto world = hlsl::transpose(tmp); + retval.worldTransforms.push_back(world); + retval.bound = hlsl::shapes::util::union_(nbl::hlsl::math::linalg::pseudo_mul(hlsl::float64_t3x4(world), aabb), retval.bound); + } + return retval; +} +} + bool MeshLoadersApp::loadModel(const system::path& modelPath, bool updateCamera, bool storeCamera) { if (modelPath.empty()) @@ -41,10 +131,9 @@ bool MeshLoadersApp::loadModel(const system::path& modelPath, bool updateCamera, m_modelPath.c_str(), loadMs, static_cast(loadResult.inputSize)); - if (asset.getContents().empty()) - failExit("Failed to load asset %s.", m_modelPath.c_str()); - - m_render.currentCpuAsset = (asset.getContents().size() == 1u) ? asset.getContents()[0] : nullptr; + if (asset.getContents().empty()) + failExit("Failed to load asset %s.", m_modelPath.c_str()); + m_render.currentCpuAsset = (asset.getContents().size() == 1u) ? asset.getContents()[0] : nullptr; core::vector> geometries; const auto extractStart = clock_t::now(); @@ -72,7 +161,21 @@ bool MeshLoadersApp::loadModel(const system::path& modelPath, bool updateCamera, { m_logger->log("%s AABB is (%f,%f,%f) -> (%f,%f,%f)", ILogger::ELL_INFO, extraMsg, aabb.minVx.x, aabb.minVx.y, aabb.minVx.z, aabb.maxVx.x, aabb.maxVx.y, aabb.maxVx.z); }; - auto bound = aabb_t::create(); + core::vector aabbs; + aabbs.reserve(geometries.size()); + for (uint32_t i = 0u; i < geometries.size(); ++i) + { + auto aabb = getGeometryAABB(geometries[i].get()); + if (!isValidAABB(aabb)) + { + m_logger->log("Invalid geometry AABB for %s (geo=%u). Using fallback unit AABB for framing.", ILogger::ELL_WARNING, m_modelPath.c_str(), i); + aabb = nbl::examples::geometry::fallbackUnitAABB(); + } + aabbs.push_back(aabb); + } + const auto layout = buildDisplayLayout(aabbs, geometries.size() > 1u); + const auto& worldTforms = layout.worldTransforms; + auto bound = layout.bound; // convert the geometries { smart_refctd_ptr converter = CAssetConverter::create({ .device = m_device.get() }); @@ -143,12 +246,6 @@ bool MeshLoadersApp::loadModel(const system::path& modelPath, bool updateCamera, failExit("Failed to await submission feature."); } - auto tmp = hlsl::float32_t4x3( - hlsl::float32_t3(1, 0, 0), - hlsl::float32_t3(0, 1, 0), - hlsl::float32_t3(0, 0, 1), - hlsl::float32_t3(0, 0, 0)); - core::vector worldTforms; const auto& converted = reservation.getGPUObjects(); m_aabbInstances.resize(converted.size()); if (m_drawBBMode == DBBM_OBB) @@ -156,17 +253,11 @@ bool MeshLoadersApp::loadModel(const system::path& modelPath, bool updateCamera, for (uint32_t i = 0; i < converted.size(); i++) { const auto& cpuGeom = geometries[i].get(); - auto promoted = getGeometryAABB(cpuGeom); - if (!isValidAABB(promoted)) - { - m_logger->log("Invalid geometry AABB for %s (geo=%u). Using fallback unit AABB for framing.", ILogger::ELL_WARNING, m_modelPath.c_str(), i); - promoted = nbl::examples::geometry::fallbackUnitAABB(); - } + const auto& promoted = aabbs[i]; printAABB(promoted, "Geometry"); - const auto promotedWorld = hlsl::float64_t3x4(worldTforms.emplace_back(hlsl::transpose(tmp))); + const auto promotedWorld = hlsl::float64_t3x4(worldTforms[i]); const auto transformed = nbl::hlsl::math::linalg::pseudo_mul(promotedWorld, promoted); printAABB(transformed, "Transformed"); - bound = hlsl::shapes::util::union_(transformed, bound); #ifdef NBL_BUILD_DEBUG_DRAW auto& aabbInst = m_aabbInstances[i]; @@ -220,10 +311,10 @@ bool MeshLoadersApp::loadModel(const system::path& modelPath, bool updateCamera, } } - auto worlTformsIt = worldTforms.begin(); + auto worldTformsIt = worldTforms.begin(); for (const auto& geo : m_render.renderer->getGeometries()) m_render.renderer->m_instances.push_back({ - .world = *(worlTformsIt++), + .world = *(worldTformsIt++), .packedGeo = &geo }); } @@ -263,11 +354,10 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) core::vector> geometries; core::vector> aabbs; - geometries.reserve(m_runtime.cases.size()); - aabbs.reserve(m_runtime.cases.size()); core::vector> cpuToConvert; - core::vector convertEntries; + struct ConvertTarget { CachedGeometryEntry* entry = nullptr; size_t geometryIx = 0u; }; + core::vector convertTargets; m_rowView.cache.reserve(m_runtime.cases.size()); @@ -283,7 +373,7 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) auto& entry = m_rowView.cache[cacheKey]; double assetLoadMs = 0.0; bool cached = true; - if (!entry.cpu) + if (entry.cpu.empty()) { stats.cpuMisses++; cached = false; @@ -299,57 +389,66 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) const auto extractStart = clock_t::now(); core::vector> found; if (appendGeometriesFromBundle(asset, found)) - { - if (!found.empty()) - entry.cpu = found.front(); - } + entry.cpu = std::move(found); stats.extractMs += toMs(clock_t::now() - extractStart); - if (!entry.cpu) + if (entry.cpu.empty()) failExit("No geometry found in asset %s.", path.string().c_str()); + entry.gpu.resize(entry.cpu.size()); const auto aabbStart = clock_t::now(); - entry.aabb = getGeometryAABB(entry.cpu.get()); - entry.hasAabb = isValidAABB(entry.aabb); - if (!entry.hasAabb) + entry.aabbs.clear(); + entry.aabbs.reserve(entry.cpu.size()); + for (uint32_t geoIx = 0u; geoIx < entry.cpu.size(); ++geoIx) { - m_logger->log("Invalid row-view geometry AABB for %s. Using fallback unit AABB.", ILogger::ELL_WARNING, path.string().c_str()); - entry.aabb = nbl::examples::geometry::fallbackUnitAABB(); - entry.hasAabb = true; + auto aabb = getGeometryAABB(entry.cpu[geoIx].get()); + if (!isValidAABB(aabb)) + { + m_logger->log("Invalid row-view geometry AABB for %s (geo=%u). Using fallback unit AABB.", ILogger::ELL_WARNING, path.string().c_str(), geoIx); + aabb = nbl::examples::geometry::fallbackUnitAABB(); + } + entry.aabbs.push_back(aabb); } stats.aabbMs += toMs(clock_t::now() - aabbStart); } else { stats.cpuHits++; - if (!entry.hasAabb) + if (entry.gpu.size() != entry.cpu.size()) + entry.gpu.resize(entry.cpu.size()); + if (entry.aabbs.size() != entry.cpu.size()) { const auto aabbStart = clock_t::now(); - entry.aabb = getGeometryAABB(entry.cpu.get()); - entry.hasAabb = isValidAABB(entry.aabb); - if (!entry.hasAabb) + entry.aabbs.clear(); + entry.aabbs.reserve(entry.cpu.size()); + for (uint32_t geoIx = 0u; geoIx < entry.cpu.size(); ++geoIx) { - m_logger->log("Invalid row-view geometry AABB for %s. Using fallback unit AABB.", ILogger::ELL_WARNING, path.string().c_str()); - entry.aabb = nbl::examples::geometry::fallbackUnitAABB(); - entry.hasAabb = true; + auto aabb = getGeometryAABB(entry.cpu[geoIx].get()); + if (!isValidAABB(aabb)) + { + m_logger->log("Invalid row-view geometry AABB for %s (geo=%u). Using fallback unit AABB.", ILogger::ELL_WARNING, path.string().c_str(), geoIx); + aabb = nbl::examples::geometry::fallbackUnitAABB(); + } + entry.aabbs.push_back(aabb); } stats.aabbMs += toMs(clock_t::now() - aabbStart); } } logRowViewAssetLoad(path, assetLoadMs, cached); - if (!entry.gpu) - { - stats.gpuMisses++; - cpuToConvert.push_back(entry.cpu); - convertEntries.push_back(&entry); - } - else + for (size_t geoIx = 0u; geoIx < entry.cpu.size(); ++geoIx) { - stats.gpuHits++; + if (!entry.gpu[geoIx]) + { + stats.gpuMisses++; + cpuToConvert.push_back(entry.cpu[geoIx]); + convertTargets.push_back({.entry = &entry,.geometryIx = geoIx}); + } + else + stats.gpuHits++; } - geometries.push_back(entry.cpu); - aabbs.push_back(entry.aabb); + geometries.insert(geometries.end(), entry.cpu.begin(), entry.cpu.end()); + aabbs.insert(aabbs.end(), entry.aabbs.begin(), entry.aabbs.end()); } if (geometries.empty()) @@ -425,27 +524,32 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) const auto& converted = reservation.getGPUObjects(); for (size_t i = 0u; i < converted.size(); ++i) - convertEntries[i]->gpu = converted[i]; + convertTargets[i].entry->gpu[convertTargets[i].geometryIx] = converted[i]; stats.convertMs = toMs(clock_t::now() - convertStart); } + const size_t totalGeometryCount = geometries.size(); size_t existingCount = m_render.renderer->getGeometries().size(); - const bool incremental = (mode == RowViewReloadMode::Incremental) && (existingCount <= m_runtime.cases.size()); + const bool incremental = (mode == RowViewReloadMode::Incremental) && (existingCount <= totalGeometryCount); if (!incremental && mode == RowViewReloadMode::Incremental) return loadRowView(RowViewReloadMode::Full); - if (mode == RowViewReloadMode::Full) + core::vector allGeometries; + allGeometries.reserve(totalGeometryCount); + for (const auto& testCase : m_runtime.cases) { - core::vector allGeometries; - allGeometries.reserve(m_runtime.cases.size()); - for (const auto& testCase : m_runtime.cases) + const auto& entry = m_rowView.cache[makeCacheKey(testCase.path)]; + for (size_t geoIx = 0u; geoIx < entry.gpu.size(); ++geoIx) { - const auto& entry = m_rowView.cache[makeCacheKey(testCase.path)]; - if (!entry.gpu) + if (!entry.gpu[geoIx]) failExit("Missing GPU geometry for %s.", testCase.path.string().c_str()); - allGeometries.push_back(entry.gpu.get()); + allGeometries.push_back(entry.gpu[geoIx].get()); } + } + + if (mode == RowViewReloadMode::Full) + { stats.addCount = allGeometries.size(); const auto addStart = clock_t::now(); if (!allGeometries.empty()) @@ -455,21 +559,12 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) } else { - const size_t addCount = (existingCount < m_runtime.cases.size()) ? (m_runtime.cases.size() - existingCount) : 0u; + const size_t addCount = (existingCount < totalGeometryCount) ? (totalGeometryCount - existingCount) : 0u; stats.addCount = addCount; if (addCount > 0u) { - core::vector newGeometries; - newGeometries.reserve(addCount); - for (size_t i = existingCount; i < m_runtime.cases.size(); ++i) - { - const auto& entry = m_rowView.cache[makeCacheKey(m_runtime.cases[i].path)]; - if (!entry.gpu) - failExit("Missing GPU geometry for %s.", m_runtime.cases[i].path.string().c_str()); - newGeometries.push_back(entry.gpu.get()); - } const auto addStart = clock_t::now(); - if (!m_render.renderer->addGeometries({ newGeometries.data(),newGeometries.size() })) + if (!m_render.renderer->addGeometries({ allGeometries.data() + existingCount,addCount })) failExit("Failed to add geometries to renderer."); stats.addGeoMs = toMs(clock_t::now() - addStart); } @@ -480,53 +575,13 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) { m_logger->log("%s AABB is (%f,%f,%f) -> (%f,%f,%f)", ILogger::ELL_INFO, extraMsg, aabb.minVx.x, aabb.minVx.y, aabb.minVx.z, aabb.maxVx.x, aabb.maxVx.y, aabb.maxVx.z); }; - auto bound = aabb_t::create(); - const auto layoutStart = clock_t::now(); - double targetExtent = 0.0; - core::vector maxDims; - maxDims.reserve(aabbs.size()); - for (const auto& aabb : aabbs) - { - const auto extent = aabb.getExtent(); - const double maxDim = std::max({ extent.x, extent.y, extent.z, 0.001 }); - maxDims.push_back(maxDim); - if (maxDim > targetExtent) - targetExtent = maxDim; - } - - core::vector scales; - scales.reserve(aabbs.size()); - for (const auto maxDim : maxDims) - scales.push_back(targetExtent / maxDim); - - double maxWidth = 0.0; - double totalWidth = 0.0; - core::vector widths; - widths.reserve(aabbs.size()); - for (size_t i = 0; i < aabbs.size(); ++i) - { - const auto extent = aabbs[i].getExtent(); - const double width = std::max(0.001, extent.x * scales[i]); - widths.push_back(width); - totalWidth += width; - if (width > maxWidth) - maxWidth = width; - } - const double spacing = std::max(0.05 * maxWidth, 0.01); - const double totalSpan = totalWidth + spacing * double(widths.size() > 0 ? widths.size() - 1 : 0); - double cursor = -0.5 * totalSpan; + const auto layout = buildDisplayLayout(aabbs, true); stats.layoutMs = toMs(clock_t::now() - layoutStart); const auto instanceStart = clock_t::now(); - auto tmp = hlsl::float32_t4x3( - hlsl::float32_t3(1, 0, 0), - hlsl::float32_t3(0, 1, 0), - hlsl::float32_t3(0, 0, 1), - hlsl::float32_t3(0, 0, 0) - ); - core::vector worldTforms; - worldTforms.reserve(geometries.size()); + const auto& worldTforms = layout.worldTransforms; + const auto& bound = layout.bound; m_aabbInstances.resize(geometries.size()); if (m_drawBBMode == DBBM_OBB) m_obbInstances.resize(geometries.size()); @@ -538,24 +593,9 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) const auto aabb = aabbs[i]; printAABB(aabb, "Geometry"); - const double scale = scales[i]; - const auto center = (aabb.minVx + aabb.maxVx) * 0.5; - const double width = widths[i]; - const double targetCenterX = cursor + 0.5 * width; - cursor += width + spacing; - - const double tx = targetCenterX - scale * center.x; - const double ty = -scale * center.y; - const double tz = -scale * center.z; - tmp[0] = hlsl::float32_t3(static_cast(scale), 0.f, 0.f); - tmp[1] = hlsl::float32_t3(0.f, static_cast(scale), 0.f); - tmp[2] = hlsl::float32_t3(0.f, 0.f, static_cast(scale)); - tmp[3] = hlsl::float32_t3(static_cast(tx), static_cast(ty), static_cast(tz)); - - const auto promotedWorld = hlsl::float64_t3x4(worldTforms.emplace_back(hlsl::transpose(tmp))); + const auto promotedWorld = hlsl::float64_t3x4(worldTforms[i]); const auto transformed = nbl::hlsl::math::linalg::pseudo_mul(promotedWorld, aabb); printAABB(transformed, "Transformed"); - bound = hlsl::shapes::util::union_(transformed, bound); #ifdef NBL_BUILD_DEBUG_DRAW auto& aabbInst = m_aabbInstances[i]; @@ -773,6 +813,9 @@ IAssetLoader::SAssetLoadParams MeshLoadersApp::makeLoadParams() const params.logger = nullptr; params.cacheFlags = IAssetLoader::ECF_DUPLICATE_TOP_LEVEL; params.ioPolicy.runtimeTuning.mode = m_runtimeTuningMode; + const bool needLoaderContentHashes = m_forceLoaderContentHashes || m_runtime.mode == RunMode::CI; + if (!needLoaderContentHashes) + params.loaderFlags |= IAssetLoader::ELPF_DONT_COMPUTE_CONTENT_HASHES; return params; } diff --git a/12_MeshLoaders/MeshLoadersAppRuntime.cpp b/12_MeshLoaders/MeshLoadersAppRuntime.cpp index 99c567dea..b653ac509 100644 --- a/12_MeshLoaders/MeshLoadersAppRuntime.cpp +++ b/12_MeshLoaders/MeshLoadersAppRuntime.cpp @@ -204,16 +204,22 @@ void MeshLoadersApp::advanceCase() if (!captureScreenshot(m_output.loadedScreenshotPath, m_render.loadedScreenshot)) failExit("Failed to capture loaded screenshot."); + const bool canWriteCurrentAsset = m_output.saveGeom && static_cast(m_render.currentCpuAsset); if (m_output.saveGeom) { - if (!m_render.currentCpuAsset) - failExit("No single root asset to write."); - if (!writeAssetRoot(m_render.currentCpuAsset, m_output.writtenPath.string())) + if (!canWriteCurrentAsset) + m_logger->log("Skipping write/reload for %s because the loaded case expands to multiple root geometries.", ILogger::ELL_INFO, m_caseName.c_str()); + else if (!writeAssetRoot(m_render.currentCpuAsset, m_output.writtenPath.string())) failExit("Geometry write failed."); } if (m_runtime.mode == RunMode::CI) { + if (!canWriteCurrentAsset) + { + advanceToNextCase(); + return; + } if (!loadModel(m_output.writtenPath, false, false)) failExit("Failed to load written asset %s.", m_output.writtenPath.string().c_str()); if (!m_render.currentCpuGeom) @@ -223,7 +229,7 @@ void MeshLoadersApp::advanceCase() return; } - if (m_output.saveGeom) + if (canWriteCurrentAsset) { if (!validateWrittenAsset(m_output.writtenPath)) failExit("Failed to load written asset %s.", m_output.writtenPath.string().c_str()); diff --git a/12_MeshLoaders/README.md b/12_MeshLoaders/README.md index c40c9dd9b..61d2672a4 100644 --- a/12_MeshLoaders/README.md +++ b/12_MeshLoaders/README.md @@ -94,7 +94,7 @@ Example for loading and writing `OBJ`, `PLY` and `STL` meshes. { "row_view": true, "cases": [ - "../media/yellowflower.obj", + "../media/cornell_box_multimaterial.obj", { "name": "spanner", "path": "../media/ply/Spanner-ply.ply" }, { "path": "../media/Stanford_Bunny.stl" } ] diff --git a/12_MeshLoaders/inputs.json b/12_MeshLoaders/inputs.json index aa1c653f2..cc450999c 100644 --- a/12_MeshLoaders/inputs.json +++ b/12_MeshLoaders/inputs.json @@ -2,7 +2,7 @@ "row_view": true, "cases": [ "../media/ply/Spanner-ply.ply", - "../media/yellowflower.obj", + "../media/cornell_box_multimaterial.obj", "../media/Stanford_Bunny.stl" ] } From 6ceae5f01095f1e04ee274a094622c5ffd0b1ba7 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Mon, 9 Mar 2026 21:07:45 +0100 Subject: [PATCH 19/34] Refine mesh loaders example flow --- 12_MeshLoaders/CMakeLists.txt | 2 +- 12_MeshLoaders/MeshLoadersAppLoad.cpp | 2 +- media | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/12_MeshLoaders/CMakeLists.txt b/12_MeshLoaders/CMakeLists.txt index d27f11688..12f3b8baa 100644 --- a/12_MeshLoaders/CMakeLists.txt +++ b/12_MeshLoaders/CMakeLists.txt @@ -100,7 +100,7 @@ endif() enable_testing() add_test(NAME NBL_MESHLOADERS_CI - COMMAND "$" --ci + COMMAND "$" --ci --loader-content-hashes WORKING_DIRECTORY "$" COMMAND_EXPAND_LISTS ) diff --git a/12_MeshLoaders/MeshLoadersAppLoad.cpp b/12_MeshLoaders/MeshLoadersAppLoad.cpp index c6ab56722..2b1d801a4 100644 --- a/12_MeshLoaders/MeshLoadersAppLoad.cpp +++ b/12_MeshLoaders/MeshLoadersAppLoad.cpp @@ -813,7 +813,7 @@ IAssetLoader::SAssetLoadParams MeshLoadersApp::makeLoadParams() const params.logger = nullptr; params.cacheFlags = IAssetLoader::ECF_DUPLICATE_TOP_LEVEL; params.ioPolicy.runtimeTuning.mode = m_runtimeTuningMode; - const bool needLoaderContentHashes = m_forceLoaderContentHashes || m_runtime.mode == RunMode::CI; + const bool needLoaderContentHashes = m_forceLoaderContentHashes; if (!needLoaderContentHashes) params.loaderFlags |= IAssetLoader::ELPF_DONT_COMPUTE_CONTENT_HASHES; return params; diff --git a/media b/media index 293f204fd..3e3b92d99 160000 --- a/media +++ b/media @@ -1 +1 @@ -Subproject commit 293f204fd0cc0c443d2c732c6adaf4b7e3f9b0d7 +Subproject commit 3e3b92d9924581bcdd8fe7b8884126b7bda10b77 From 540fc1ab50442be3009a3ba6611e1f6d36ceb740 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Tue, 10 Mar 2026 14:16:53 +0100 Subject: [PATCH 20/34] Handle scene assets in mesh loaders example --- 12_MeshLoaders/MeshLoadersAppRuntime.cpp | 42 ++++++++++++++++++------ 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/12_MeshLoaders/MeshLoadersAppRuntime.cpp b/12_MeshLoaders/MeshLoadersAppRuntime.cpp index b653ac509..cb99028df 100644 --- a/12_MeshLoaders/MeshLoadersAppRuntime.cpp +++ b/12_MeshLoaders/MeshLoadersAppRuntime.cpp @@ -142,6 +142,24 @@ bool MeshLoadersApp::appendGeometriesFromBundle(const asset::SAssetBundle& bundl if (bundle.getContents().empty()) return false; + auto appendCollection = [&](const ICPUGeometryCollection* collection) -> void + { + if (!collection) + return; + const auto& refs = collection->getGeometries(); + for (const auto& ref : refs) + { + if (!ref.geometry) + continue; + if (ref.geometry->getPrimitiveType() != IGeometryBase::EPrimitiveType::Polygon) + continue; + const auto assetRef = core::smart_refctd_ptr_static_cast(ref.geometry); + auto poly = IAsset::castDown(assetRef); + if (poly) + out.push_back(poly); + } + }; + switch (bundle.getAssetType()) { case IAsset::E_TYPE::ET_GEOMETRY: @@ -155,19 +173,23 @@ bool MeshLoadersApp::appendGeometriesFromBundle(const asset::SAssetBundle& bundl for (const auto& item : bundle.getContents()) { auto collection = IAsset::castDown(item); - if (!collection) + appendCollection(collection.get()); + } + break; + case IAsset::E_TYPE::ET_SCENE: + for (const auto& item : bundle.getContents()) + { + auto scene = IAsset::castDown(item); + if (!scene) continue; - const auto& refs = collection->getGeometries(); - for (const auto& ref : refs) + const auto& instances = scene->getInstances().getMorphTargets(); + for (const auto& morphTargets : instances) { - if (!ref.geometry) - continue; - if (ref.geometry->getPrimitiveType() != IGeometryBase::EPrimitiveType::Polygon) + if (!morphTargets) continue; - const auto assetRef = core::smart_refctd_ptr_static_cast(ref.geometry); - auto poly = IAsset::castDown(assetRef); - if (poly) - out.push_back(poly); + const auto& targets = *morphTargets->getTargets(); + for (const auto& target : targets) + appendCollection(target.geoCollection.get()); } } break; From 653bbdc171fe7ab7891f6c29e063bc22d327c244 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Tue, 10 Mar 2026 16:12:40 +0100 Subject: [PATCH 21/34] Refine mesh loaders scene example --- .../{MeshLoadersApp.hpp => App.hpp} | 12 +- ...adersAppLifecycle.cpp => AppLifecycle.cpp} | 38 ++++++- .../{MeshLoadersAppLoad.cpp => AppLoad.cpp} | 107 +++++++++++++++--- ...shLoadersAppRuntime.cpp => AppRuntime.cpp} | 71 +++++++++++- 12_MeshLoaders/CMakeLists.txt | 8 +- 12_MeshLoaders/README.md | 25 ++-- 12_MeshLoaders/inputs.json | 2 +- 12_MeshLoaders/main.cpp | 2 +- 8 files changed, 230 insertions(+), 35 deletions(-) rename 12_MeshLoaders/{MeshLoadersApp.hpp => App.hpp} (94%) rename 12_MeshLoaders/{MeshLoadersAppLifecycle.cpp => AppLifecycle.cpp} (95%) rename 12_MeshLoaders/{MeshLoadersAppLoad.cpp => AppLoad.cpp} (89%) rename 12_MeshLoaders/{MeshLoadersAppRuntime.cpp => AppRuntime.cpp} (76%) diff --git a/12_MeshLoaders/MeshLoadersApp.hpp b/12_MeshLoaders/App.hpp similarity index 94% rename from 12_MeshLoaders/MeshLoadersApp.hpp rename to 12_MeshLoaders/App.hpp index 5fadeb8ad..bf180cfb8 100644 --- a/12_MeshLoaders/MeshLoadersApp.hpp +++ b/12_MeshLoaders/App.hpp @@ -81,6 +81,14 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built core::vector> cpu; core::vector> gpu; core::vector> aabbs; + core::vector world; + core::vector preserveWorld; + }; + + struct LoadedGeometryInstance + { + smart_refctd_ptr geometry; + hlsl::float32_t3x4 world = hlsl::math::linalg::identity(); }; struct RowViewPerfStats @@ -199,6 +207,7 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built bool isWriteExtensionSupported(const std::string& ext) const; system::path resolveSavePath(const system::path& modelPath) const; static system::path resolveRuntimeCWD(const system::path& preferred); + static system::path resolveDefaultTestListPath(const system::path& effectiveInputCWD, const core::vector& argv); bool parseCommandLineOptions(const system::path& effectiveInputCWD, const system::path& effectiveOutputCWD, const system::path& defaultBenchmarkTestListPath); std::string makeCaptionModelPath() const; @@ -238,6 +247,7 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built bool validateWrittenAsset(const system::path& path); bool captureScreenshot(const system::path& path, core::smart_refctd_ptr& outImage); bool appendGeometriesFromBundle(const asset::SAssetBundle& bundle, core::vector>& out) const; + bool appendGeometryInstancesFromBundle(const asset::SAssetBundle& bundle, core::vector& out) const; bool compareImages( const asset::ICPUImageView* a, const asset::ICPUImageView* b, @@ -280,7 +290,7 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built smart_refctd_ptr m_assetLoadLogger; smart_refctd_ptr m_loaderPerfLogger; asset::SFileIOPolicy::SRuntimeTuning::Mode m_runtimeTuningMode = asset::SFileIOPolicy::SRuntimeTuning::Mode::Heuristic; - bool m_forceLoaderContentHashes = false; + bool m_forceLoaderContentHashes = true; bool m_updateGeometryHashReferences = false; std::optional m_referenceCamera; diff --git a/12_MeshLoaders/MeshLoadersAppLifecycle.cpp b/12_MeshLoaders/AppLifecycle.cpp similarity index 95% rename from 12_MeshLoaders/MeshLoadersAppLifecycle.cpp rename to 12_MeshLoaders/AppLifecycle.cpp index 8ede7df63..e93f4c3a0 100644 --- a/12_MeshLoaders/MeshLoadersAppLifecycle.cpp +++ b/12_MeshLoaders/AppLifecycle.cpp @@ -5,7 +5,7 @@ #include "argparse/argparse.hpp" #include "portable-file-dialogs/portable-file-dialogs.h" #include "nlohmann/json.hpp" -#include "MeshLoadersApp.hpp" +#include "App.hpp" #include #include @@ -41,7 +41,7 @@ void setupMeshLoadersArgumentParser(argparse::ArgumentParser& parser) .flag(); parser.add_argument("--testlist") .nargs(1) - .help("JSON file with test cases. Relative paths are resolved against local input CWD."); + .help("JSON file with test cases. Relative JSON path resolves against local input CWD. Relative case paths inside the JSON resolve against the JSON file directory."); parser.add_argument("--row-add") .nargs(1) .help("Add a model path to row view on startup without using a dialog."); @@ -52,7 +52,7 @@ void setupMeshLoadersArgumentParser(argparse::ArgumentParser& parser) .nargs(1) .help("Write loader diagnostics to a file instead of stdout."); parser.add_argument("--loader-content-hashes") - .help("Force loaders to compute CPU buffer content hashes before returning.") + .help("Keep loader content hashes enabled. This is already the default for this example.") .flag(); parser.add_argument("--update-references") .help("Accept official benchmark/reference CLI without changing current local flow.") @@ -103,6 +103,36 @@ system::path MeshLoadersApp::resolveRuntimeCWD(const system::path& preferred) return preferred; } +system::path MeshLoadersApp::resolveDefaultTestListPath(const system::path& effectiveInputCWD, const core::vector& argv) +{ + const auto tryExisting = [](std::filesystem::path candidate) -> std::optional + { + std::error_code ec; + candidate = candidate.lexically_normal(); + if (!candidate.empty() && std::filesystem::exists(candidate, ec) && !ec) + return system::path(candidate.generic_string()); + return std::nullopt; + }; + + if (auto resolved = tryExisting(effectiveInputCWD / "inputs.json"); resolved.has_value()) + return *resolved; + + if (!argv.empty() && !argv[0].empty()) + { + std::error_code ec; + auto exePath = std::filesystem::absolute(std::filesystem::path(argv[0]), ec); + if (!ec) + { + if (auto resolved = tryExisting(exePath.parent_path() / ".." / "inputs.json"); resolved.has_value()) + return *resolved; + if (auto resolved = tryExisting(exePath.parent_path() / "inputs.json"); resolved.has_value()) + return *resolved; + } + } + + return (effectiveInputCWD / "inputs.json").lexically_normal(); +} + std::string MeshLoadersApp::makeCaptionModelPath() const { const auto& modelPath = m_modelPath; @@ -207,7 +237,7 @@ bool MeshLoadersApp::parseCommandLineOptions(const system::path& effectiveInputC m_runtime.mode = RunMode::Batch; m_output.saveGeomPrefixPath = effectiveOutputCWD / "saved"; m_output.screenshotPrefixPath = effectiveOutputCWD / "screenshots"; - m_output.testListPath = effectiveInputCWD / "inputs.json"; + m_output.testListPath = resolveDefaultTestListPath(effectiveInputCWD, argv); m_runtime.forceRowViewForCurrentTestList = false; argparse::ArgumentParser parser("12_meshloaders"); diff --git a/12_MeshLoaders/MeshLoadersAppLoad.cpp b/12_MeshLoaders/AppLoad.cpp similarity index 89% rename from 12_MeshLoaders/MeshLoadersAppLoad.cpp rename to 12_MeshLoaders/AppLoad.cpp index 2b1d801a4..c0bfca432 100644 --- a/12_MeshLoaders/MeshLoadersAppLoad.cpp +++ b/12_MeshLoaders/AppLoad.cpp @@ -2,7 +2,7 @@ // This file is part of the "Nabla Engine". // For conditions of distribution and use, see copyright notice in nabla.h -#include "MeshLoadersApp.hpp" +#include "App.hpp" #include #include @@ -136,8 +136,18 @@ bool MeshLoadersApp::loadModel(const system::path& modelPath, bool updateCamera, m_render.currentCpuAsset = (asset.getContents().size() == 1u) ? asset.getContents()[0] : nullptr; core::vector> geometries; + core::vector geometryInstances; const auto extractStart = clock_t::now(); - if (!appendGeometriesFromBundle(asset, geometries)) + const bool renderAsScene = asset.getAssetType() == IAsset::E_TYPE::ET_SCENE; + if (renderAsScene) + { + if (!appendGeometryInstancesFromBundle(asset, geometryInstances)) + failExit("Asset loaded but not a supported type for %s.", m_modelPath.c_str()); + geometries.reserve(geometryInstances.size()); + for (const auto& instance : geometryInstances) + geometries.push_back(instance.geometry); + } + else if (!appendGeometriesFromBundle(asset, geometries)) failExit("Asset loaded but not a supported type for %s.", m_modelPath.c_str()); const auto extractMs = toMs(clock_t::now() - extractStart); if (geometries.empty()) @@ -173,9 +183,24 @@ bool MeshLoadersApp::loadModel(const system::path& modelPath, bool updateCamera, } aabbs.push_back(aabb); } - const auto layout = buildDisplayLayout(aabbs, geometries.size() > 1u); - const auto& worldTforms = layout.worldTransforms; - auto bound = layout.bound; + core::vector worldTforms; + worldTforms.reserve(geometries.size()); + auto bound = display_aabb_t::create(); + if (renderAsScene) + { + for (uint32_t i = 0u; i < geometryInstances.size(); ++i) + { + const auto& world = geometryInstances[i].world; + worldTforms.push_back(world); + bound = hlsl::shapes::util::union_(nbl::hlsl::math::linalg::pseudo_mul(hlsl::float64_t3x4(world), aabbs[i]), bound); + } + } + else + { + const auto layout = buildDisplayLayout(aabbs, geometries.size() > 1u); + worldTforms = layout.worldTransforms; + bound = layout.bound; + } // convert the geometries { smart_refctd_ptr converter = CAssetConverter::create({ .device = m_device.get() }); @@ -354,6 +379,8 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) core::vector> geometries; core::vector> aabbs; + core::vector sourceWorlds; + core::vector preserveWorlds; core::vector> cpuToConvert; struct ConvertTarget { CachedGeometryEntry* entry = nullptr; size_t geometryIx = 0u; }; @@ -387,9 +414,33 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) failExit("Failed to load asset %s.", path.string().c_str()); const auto extractStart = clock_t::now(); - core::vector> found; - if (appendGeometriesFromBundle(asset, found)) - entry.cpu = std::move(found); + entry.world.clear(); + entry.preserveWorld.clear(); + if (asset.getAssetType() == IAsset::E_TYPE::ET_SCENE) + { + core::vector found; + if (appendGeometryInstancesFromBundle(asset, found)) + { + entry.cpu.reserve(found.size()); + entry.world.reserve(found.size()); + entry.preserveWorld.assign(found.size(), 1u); + for (auto& instance : found) + { + entry.cpu.push_back(instance.geometry); + entry.world.push_back(instance.world); + } + } + } + else + { + core::vector> found; + if (appendGeometriesFromBundle(asset, found)) + { + entry.cpu = std::move(found); + entry.world.assign(entry.cpu.size(), hlsl::math::linalg::identity()); + entry.preserveWorld.assign(entry.cpu.size(), 0u); + } + } stats.extractMs += toMs(clock_t::now() - extractStart); if (entry.cpu.empty()) failExit("No geometry found in asset %s.", path.string().c_str()); @@ -415,6 +466,10 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) stats.cpuHits++; if (entry.gpu.size() != entry.cpu.size()) entry.gpu.resize(entry.cpu.size()); + if (entry.world.size() != entry.cpu.size()) + entry.world.assign(entry.cpu.size(), hlsl::math::linalg::identity()); + if (entry.preserveWorld.size() != entry.cpu.size()) + entry.preserveWorld.assign(entry.cpu.size(), 0u); if (entry.aabbs.size() != entry.cpu.size()) { const auto aabbStart = clock_t::now(); @@ -449,6 +504,8 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) geometries.insert(geometries.end(), entry.cpu.begin(), entry.cpu.end()); aabbs.insert(aabbs.end(), entry.aabbs.begin(), entry.aabbs.end()); + sourceWorlds.insert(sourceWorlds.end(), entry.world.begin(), entry.world.end()); + preserveWorlds.insert(preserveWorlds.end(), entry.preserveWorld.begin(), entry.preserveWorld.end()); } if (geometries.empty()) @@ -576,12 +633,37 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) m_logger->log("%s AABB is (%f,%f,%f) -> (%f,%f,%f)", ILogger::ELL_INFO, extraMsg, aabb.minVx.x, aabb.minVx.y, aabb.minVx.z, aabb.maxVx.x, aabb.maxVx.y, aabb.maxVx.z); }; const auto layoutStart = clock_t::now(); - const auto layout = buildDisplayLayout(aabbs, true); + core::vector worldTforms; + worldTforms.resize(geometries.size()); + auto bound = aabb_t::create(); + core::vector rowAABBs; + core::vector rowIndices; + rowAABBs.reserve(aabbs.size()); + rowIndices.reserve(aabbs.size()); + for (uint32_t i = 0u; i < aabbs.size(); ++i) + { + if (i < preserveWorlds.size() && preserveWorlds[i]) + { + worldTforms[i] = sourceWorlds[i]; + bound = hlsl::shapes::util::union_(nbl::hlsl::math::linalg::pseudo_mul(hlsl::float64_t3x4(worldTforms[i]), aabbs[i]), bound); + continue; + } + rowIndices.push_back(i); + rowAABBs.push_back(aabbs[i]); + } + if (!rowIndices.empty()) + { + const auto layout = buildDisplayLayout(rowAABBs, true); + for (uint32_t i = 0u; i < rowIndices.size(); ++i) + { + const uint32_t geometryIx = rowIndices[i]; + worldTforms[geometryIx] = layout.worldTransforms[i]; + bound = hlsl::shapes::util::union_(nbl::hlsl::math::linalg::pseudo_mul(hlsl::float64_t3x4(worldTforms[geometryIx]), aabbs[geometryIx]), bound); + } + } stats.layoutMs = toMs(clock_t::now() - layoutStart); const auto instanceStart = clock_t::now(); - const auto& worldTforms = layout.worldTransforms; - const auto& bound = layout.bound; m_aabbInstances.resize(geometries.size()); if (m_drawBBMode == DBBM_OBB) m_obbInstances.resize(geometries.size()); @@ -813,9 +895,6 @@ IAssetLoader::SAssetLoadParams MeshLoadersApp::makeLoadParams() const params.logger = nullptr; params.cacheFlags = IAssetLoader::ECF_DUPLICATE_TOP_LEVEL; params.ioPolicy.runtimeTuning.mode = m_runtimeTuningMode; - const bool needLoaderContentHashes = m_forceLoaderContentHashes; - if (!needLoaderContentHashes) - params.loaderFlags |= IAssetLoader::ELPF_DONT_COMPUTE_CONTENT_HASHES; return params; } diff --git a/12_MeshLoaders/MeshLoadersAppRuntime.cpp b/12_MeshLoaders/AppRuntime.cpp similarity index 76% rename from 12_MeshLoaders/MeshLoadersAppRuntime.cpp rename to 12_MeshLoaders/AppRuntime.cpp index cb99028df..2db84d668 100644 --- a/12_MeshLoaders/MeshLoadersAppRuntime.cpp +++ b/12_MeshLoaders/AppRuntime.cpp @@ -2,10 +2,11 @@ // This file is part of the "Nabla Engine". // For conditions of distribution and use, see copyright notice in nabla.h -#include "MeshLoadersApp.hpp" +#include "App.hpp" #include "nbl/ext/ScreenShot/ScreenShot.h" #include "nbl/examples/common/ImageComparison.h" +#include "nbl/builtin/hlsl/math/linalg/fast_affine.hlsl" std::string MeshLoadersApp::makeUniqueCaseName(const system::path& path) { @@ -200,6 +201,74 @@ bool MeshLoadersApp::appendGeometriesFromBundle(const asset::SAssetBundle& bundl return !out.empty(); } +bool MeshLoadersApp::appendGeometryInstancesFromBundle(const asset::SAssetBundle& bundle, core::vector& out) const +{ + if (bundle.getContents().empty()) + return false; + const auto identity = hlsl::math::linalg::identity(); + auto appendCollection = [&](const ICPUGeometryCollection* collection, const hlsl::float32_t3x4& baseTransform, const bool preserveReferenceTransforms) -> void + { + if (!collection) + return; + const auto& refs = collection->getGeometries(); + for (const auto& ref : refs) + { + if (!ref.geometry || ref.geometry->getPrimitiveType() != IGeometryBase::EPrimitiveType::Polygon) + continue; + const auto assetRef = core::smart_refctd_ptr_static_cast(ref.geometry); + auto poly = IAsset::castDown(assetRef); + if (!poly) + continue; + LoadedGeometryInstance instance = {.geometry = std::move(poly),.world = baseTransform}; + if (preserveReferenceTransforms && ref.hasTransform()) + instance.world = hlsl::math::linalg::promoted_mul(baseTransform, ref.transform); + out.push_back(std::move(instance)); + } + }; + switch (bundle.getAssetType()) + { + case IAsset::E_TYPE::ET_GEOMETRY: + for (const auto& item : bundle.getContents()) + { + auto polyGeo = IAsset::castDown(item); + if (polyGeo) + out.push_back({.geometry = std::move(polyGeo),.world = identity}); + } + break; + case IAsset::E_TYPE::ET_GEOMETRY_COLLECTION: + for (const auto& item : bundle.getContents()) + { + auto collection = IAsset::castDown(item); + appendCollection(collection.get(), identity, false); + } + break; + case IAsset::E_TYPE::ET_SCENE: + for (const auto& item : bundle.getContents()) + { + auto scene = IAsset::castDown(item); + if (!scene) + continue; + const auto& instances = scene->getInstances(); + const auto& morphTargets = instances.getMorphTargets(); + const auto& initialTransforms = instances.getInitialTransforms(); + for (uint32_t instanceIx = 0u; instanceIx < morphTargets.size(); ++instanceIx) + { + const auto* targets = morphTargets[instanceIx].get(); + if (!targets) + continue; + const auto instanceTransform = initialTransforms.empty() ? identity : initialTransforms[instanceIx]; + const auto& targetList = targets->getTargets(); + for (const auto& target : targetList) + appendCollection(target.geoCollection.get(), instanceTransform, true); + } + } + break; + default: + return false; + } + return !out.empty(); +} + bool MeshLoadersApp::compareImages( const asset::ICPUImageView* a, const asset::ICPUImageView* b, diff --git a/12_MeshLoaders/CMakeLists.txt b/12_MeshLoaders/CMakeLists.txt index 12f3b8baa..1e331fb46 100644 --- a/12_MeshLoaders/CMakeLists.txt +++ b/12_MeshLoaders/CMakeLists.txt @@ -1,9 +1,9 @@ set(SRCs main.cpp - MeshLoadersApp.hpp - MeshLoadersAppLifecycle.cpp - MeshLoadersAppLoad.cpp - MeshLoadersAppRuntime.cpp + App.hpp + AppLifecycle.cpp + AppLoad.cpp + AppRuntime.cpp inputs.json README.md ) diff --git a/12_MeshLoaders/README.md b/12_MeshLoaders/README.md index 61d2672a4..d5b489ab3 100644 --- a/12_MeshLoaders/README.md +++ b/12_MeshLoaders/README.md @@ -8,19 +8,21 @@ Example for loading and writing `OBJ`, `PLY` and `STL` meshes. - Default tuning: `heuristic` - Output meshes: `saved/` - Output screenshots: `screenshots/` +- Default startup resolves `inputs.json` from the example directory layout and is not tied to the current working directory ## Mode cheat sheet - `batch` - Uses test list and runs normal workflow. - - If test list has `row_view: true`, renders all cases in one scene. + - If test list has `row_view: true`, geometry assets are laid out in one inspection scene. - `interactive` - Opens file dialog and loads one model. - `ci` - Runs strict pass/fail validation per case. ## Row view concept -- `row_view` means one scene containing all cases from the test list. -- Each case is normalized and laid out left-to-right so camera framing is stable for comparisons. +- `row_view` means one inspection scene containing all geometry cases from the test list. +- `geometry` and `geometry collection` assets are normalized and laid out left-to-right so camera framing is stable for comparisons. +- `scene` assets keep their authored instance transforms and are rendered as scenes rather than being rewritten into row layout. ## Common workflows - Quick visual check: @@ -57,9 +59,6 @@ Example for loading and writing `OBJ`, `PLY` and `STL` meshes. - Run default CI directly via `ctest` (no benchmark datasets enabled): - `ctest --output-on-failure -C Debug -R ^NBL_MESHLOADERS_CI$` - uses default `inputs.json` (3 inputs) -- Hash parity tests now live in Nabla tool: - - `ctest --output-on-failure -C Debug -R NBL_HCP` - - tool: `tools/hcp` (headless, dummy-blob based parity checks) ## CLI - `--ci` @@ -68,16 +67,22 @@ Example for loading and writing `OBJ`, `PLY` and `STL` meshes. - file-dialog run - `--testlist ` - custom JSON list + - relative JSON path resolves against local input CWD + - relative case paths inside the JSON resolve against the JSON file directory - `--savegeometry` - keep writing output meshes - `--savepath ` - force output path - `--row-add ` - add model to row view at startup + - scene assets added this way still keep authored transforms - `--row-duplicate ` - duplicate last row-view case - `--loader-perf-log ` - redirect loader diagnostics +- `--loader-content-hashes` + - keep loader content hashes enabled + - this is already the default for this example - `--runtime-tuning ` - IO runtime tuning mode @@ -86,8 +91,8 @@ Example for loading and writing `OBJ`, `PLY` and `STL` meshes. - Left mouse drag: rotate camera - `Home`: reset view - `A`: add model to row view -- `X`: clear row view (empty scene) -- `R`: reload row view from test list +- `X`: clear row view inspection scene +- `R`: reload current test list or interactive model ## Input list format (`inputs.json`) ```json @@ -105,11 +110,13 @@ Rules: - `cases` is required and must be an array - case item can be string path or object with `path` and optional `name` - relative paths resolve against JSON file directory +- default startup uses `inputs.json` resolved from the example directory layout rather than the process working directory +- `row_view: true` affects geometry assets only. Scene assets keep their authored transforms. ## What CI validates - Per-case image consistency: - `*_loaded.png` vs `*_written.png` code-unit diff - - thresholds come from `MaxImageDiffCodeUnits` and `MaxImageDiffCodeUnitValue` in `MeshLoadersApp.hpp` + - thresholds come from `MaxImageDiffCodeUnits` and `MaxImageDiffCodeUnitValue` in `App.hpp` - Any mismatch ends with non-zero exit code ## Performance logs to trust diff --git a/12_MeshLoaders/inputs.json b/12_MeshLoaders/inputs.json index cc450999c..482ffdac4 100644 --- a/12_MeshLoaders/inputs.json +++ b/12_MeshLoaders/inputs.json @@ -1,8 +1,8 @@ { "row_view": true, "cases": [ - "../media/ply/Spanner-ply.ply", "../media/cornell_box_multimaterial.obj", + "../media/ply/Spanner-ply.ply", "../media/Stanford_Bunny.stl" ] } diff --git a/12_MeshLoaders/main.cpp b/12_MeshLoaders/main.cpp index ee9ab7ef4..b14581b0d 100644 --- a/12_MeshLoaders/main.cpp +++ b/12_MeshLoaders/main.cpp @@ -2,6 +2,6 @@ // This file is part of the "Nabla Engine". // For conditions of distribution and use, see copyright notice in nabla.h -#include "MeshLoadersApp.hpp" +#include "App.hpp" NBL_MAIN_FUNC(MeshLoadersApp) From 328c4fbfc7ea73178d17814dc430b573726be3ed Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Tue, 10 Mar 2026 17:55:56 +0100 Subject: [PATCH 22/34] Frame mesh loaders row view scenes --- 12_MeshLoaders/App.hpp | 3 +- 12_MeshLoaders/AppLoad.cpp | 157 ++++++++++++++++++++++++++----------- 12_MeshLoaders/README.md | 10 +-- 3 files changed, 120 insertions(+), 50 deletions(-) diff --git a/12_MeshLoaders/App.hpp b/12_MeshLoaders/App.hpp index bf180cfb8..ff41f0d78 100644 --- a/12_MeshLoaders/App.hpp +++ b/12_MeshLoaders/App.hpp @@ -82,7 +82,8 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built core::vector> gpu; core::vector> aabbs; core::vector world; - core::vector preserveWorld; + hlsl::shapes::AABB<3, double> tileAABB = hlsl::shapes::AABB<3, double>::create(); + bool layoutAsSingleTile = false; }; struct LoadedGeometryInstance diff --git a/12_MeshLoaders/AppLoad.cpp b/12_MeshLoaders/AppLoad.cpp index c0bfca432..d91f91925 100644 --- a/12_MeshLoaders/AppLoad.cpp +++ b/12_MeshLoaders/AppLoad.cpp @@ -7,7 +7,6 @@ #include #include #include - #include #include #include "nbl/examples/common/GeometryAABBUtilities.h" @@ -20,6 +19,14 @@ struct DisplayLayout core::vector worldTransforms = {}; display_aabb_t bound = display_aabb_t::create(); }; +struct RowLayoutGroup +{ + size_t firstGeometry = 0u; + size_t geometryCount = 0u; + display_aabb_t layoutAABB = display_aabb_t::create(); + bool preserveInternalTransforms = false; + bool addAggregateDebugAABB = false; +}; static hlsl::float32_t3x4 makeIdentityWorld() { auto tmp = hlsl::float32_t4x3( @@ -29,6 +36,29 @@ static hlsl::float32_t3x4 makeIdentityWorld() hlsl::float32_t3(0, 0, 0)); return hlsl::transpose(tmp); } +static hlsl::float32_t4x4 makeAffine4x4(const hlsl::float32_t3x4& world) +{ + return hlsl::float32_t4x4{ + world[0], + world[1], + world[2], + hlsl::float32_t4(0, 0, 0, 1) + }; +} +#ifdef NBL_BUILD_DEBUG_DRAW +static ext::debug_draw::InstanceData makeAABBInstance( + const display_aabb_t& aabb, + const hlsl::float32_t3x4& world, + const hlsl::float32_t4& color) +{ + ext::debug_draw::InstanceData instance = {}; + instance.color = color; + instance.transform = math::linalg::promoted_mul( + makeAffine4x4(world), + ext::debug_draw::DrawAABB::getTransformFromAABB(shapes::AABB<3, float>(aabb.minVx, aabb.maxVx))); + return instance; +} +#endif static DisplayLayout buildDisplayLayout(const core::vector& aabbs, const bool arrangeInRow) { DisplayLayout retval = {}; @@ -380,7 +410,7 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) core::vector> geometries; core::vector> aabbs; core::vector sourceWorlds; - core::vector preserveWorlds; + core::vector layoutGroups; core::vector> cpuToConvert; struct ConvertTarget { CachedGeometryEntry* entry = nullptr; size_t geometryIx = 0u; }; @@ -389,6 +419,21 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) m_rowView.cache.reserve(m_runtime.cases.size()); IAssetLoader::SAssetLoadParams params = makeLoadParams(); + auto rebuildTileAABB = [&](CachedGeometryEntry& entry, const system::path& path) -> void + { + entry.tileAABB = display_aabb_t::create(); + if (!entry.layoutAsSingleTile) + return; + for (uint32_t geoIx = 0u; geoIx < entry.aabbs.size(); ++geoIx) + entry.tileAABB = hlsl::shapes::util::union_( + nbl::hlsl::math::linalg::pseudo_mul(hlsl::float64_t3x4(entry.world[geoIx]), entry.aabbs[geoIx]), + entry.tileAABB); + if (!isValidAABB(entry.tileAABB)) + { + m_logger->log("Invalid row-view scene AABB for %s. Using fallback unit AABB.", ILogger::ELL_WARNING, path.string().c_str()); + entry.tileAABB = nbl::examples::geometry::fallbackUnitAABB(); + } + }; for (const auto& testCase : m_runtime.cases) { @@ -415,7 +460,7 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) const auto extractStart = clock_t::now(); entry.world.clear(); - entry.preserveWorld.clear(); + entry.layoutAsSingleTile = (asset.getAssetType() == IAsset::E_TYPE::ET_SCENE); if (asset.getAssetType() == IAsset::E_TYPE::ET_SCENE) { core::vector found; @@ -423,7 +468,6 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) { entry.cpu.reserve(found.size()); entry.world.reserve(found.size()); - entry.preserveWorld.assign(found.size(), 1u); for (auto& instance : found) { entry.cpu.push_back(instance.geometry); @@ -438,7 +482,6 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) { entry.cpu = std::move(found); entry.world.assign(entry.cpu.size(), hlsl::math::linalg::identity()); - entry.preserveWorld.assign(entry.cpu.size(), 0u); } } stats.extractMs += toMs(clock_t::now() - extractStart); @@ -459,6 +502,7 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) } entry.aabbs.push_back(aabb); } + rebuildTileAABB(entry, path); stats.aabbMs += toMs(clock_t::now() - aabbStart); } else @@ -468,8 +512,6 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) entry.gpu.resize(entry.cpu.size()); if (entry.world.size() != entry.cpu.size()) entry.world.assign(entry.cpu.size(), hlsl::math::linalg::identity()); - if (entry.preserveWorld.size() != entry.cpu.size()) - entry.preserveWorld.assign(entry.cpu.size(), 0u); if (entry.aabbs.size() != entry.cpu.size()) { const auto aabbStart = clock_t::now(); @@ -487,6 +529,7 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) } stats.aabbMs += toMs(clock_t::now() - aabbStart); } + rebuildTileAABB(entry, path); } logRowViewAssetLoad(path, assetLoadMs, cached); @@ -502,10 +545,33 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) stats.gpuHits++; } + const size_t firstGeometry = geometries.size(); geometries.insert(geometries.end(), entry.cpu.begin(), entry.cpu.end()); aabbs.insert(aabbs.end(), entry.aabbs.begin(), entry.aabbs.end()); sourceWorlds.insert(sourceWorlds.end(), entry.world.begin(), entry.world.end()); - preserveWorlds.insert(preserveWorlds.end(), entry.preserveWorld.begin(), entry.preserveWorld.end()); + if (entry.layoutAsSingleTile) + { + layoutGroups.push_back({ + .firstGeometry = firstGeometry, + .geometryCount = entry.cpu.size(), + .layoutAABB = entry.tileAABB, + .preserveInternalTransforms = true, + .addAggregateDebugAABB = true + }); + } + else + { + for (size_t geoIx = 0u; geoIx < entry.cpu.size(); ++geoIx) + { + layoutGroups.push_back({ + .firstGeometry = firstGeometry + geoIx, + .geometryCount = 1u, + .layoutAABB = entry.aabbs[geoIx], + .preserveInternalTransforms = false, + .addAggregateDebugAABB = false + }); + } + } } if (geometries.empty()) @@ -636,37 +702,48 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) core::vector worldTforms; worldTforms.resize(geometries.size()); auto bound = aabb_t::create(); - core::vector rowAABBs; - core::vector rowIndices; - rowAABBs.reserve(aabbs.size()); - rowIndices.reserve(aabbs.size()); - for (uint32_t i = 0u; i < aabbs.size(); ++i) + core::vector tileAABBs; + tileAABBs.reserve(layoutGroups.size()); + for (const auto& group : layoutGroups) + tileAABBs.push_back(group.layoutAABB); + const auto layout = buildDisplayLayout(tileAABBs, true); +#ifdef NBL_BUILD_DEBUG_DRAW + struct AggregateDebugAABB { - if (i < preserveWorlds.size() && preserveWorlds[i]) - { - worldTforms[i] = sourceWorlds[i]; - bound = hlsl::shapes::util::union_(nbl::hlsl::math::linalg::pseudo_mul(hlsl::float64_t3x4(worldTforms[i]), aabbs[i]), bound); - continue; - } - rowIndices.push_back(i); - rowAABBs.push_back(aabbs[i]); - } - if (!rowIndices.empty()) + aabb_t aabb = aabb_t::create(); + hlsl::float32_t3x4 world = makeIdentityWorld(); + }; + core::vector sceneDebugAABBs; + sceneDebugAABBs.reserve(layoutGroups.size()); +#endif + for (uint32_t groupIx = 0u; groupIx < layoutGroups.size(); ++groupIx) { - const auto layout = buildDisplayLayout(rowAABBs, true); - for (uint32_t i = 0u; i < rowIndices.size(); ++i) + const auto& group = layoutGroups[groupIx]; + const auto& tileWorld = layout.worldTransforms[groupIx]; +#ifdef NBL_BUILD_DEBUG_DRAW + if (group.addAggregateDebugAABB) + sceneDebugAABBs.push_back({.aabb = group.layoutAABB,.world = tileWorld}); +#endif + for (uint32_t localIx = 0u; localIx < group.geometryCount; ++localIx) { - const uint32_t geometryIx = rowIndices[i]; - worldTforms[geometryIx] = layout.worldTransforms[i]; - bound = hlsl::shapes::util::union_(nbl::hlsl::math::linalg::pseudo_mul(hlsl::float64_t3x4(worldTforms[geometryIx]), aabbs[geometryIx]), bound); + const uint32_t geometryIx = static_cast(group.firstGeometry + localIx); + worldTforms[geometryIx] = group.preserveInternalTransforms ? + hlsl::math::linalg::promoted_mul(tileWorld, sourceWorlds[geometryIx]) : + tileWorld; + bound = hlsl::shapes::util::union_( + nbl::hlsl::math::linalg::pseudo_mul(hlsl::float64_t3x4(worldTforms[geometryIx]), aabbs[geometryIx]), + bound); } } stats.layoutMs = toMs(clock_t::now() - layoutStart); const auto instanceStart = clock_t::now(); - m_aabbInstances.resize(geometries.size()); +#ifdef NBL_BUILD_DEBUG_DRAW + m_aabbInstances.clear(); + m_aabbInstances.reserve(geometries.size() + sceneDebugAABBs.size()); if (m_drawBBMode == DBBM_OBB) m_obbInstances.resize(geometries.size()); +#endif m_render.renderer->m_instances.clear(); for (uint32_t i = 0; i < geometries.size(); i++) @@ -680,18 +757,7 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) printAABB(transformed, "Transformed"); #ifdef NBL_BUILD_DEBUG_DRAW - auto& aabbInst = m_aabbInstances[i]; - const auto tmpAabb = shapes::AABB<3, float>(aabb.minVx, aabb.maxVx); - hlsl::float32_t3x4 aabbTransform = ext::debug_draw::DrawAABB::getTransformFromAABB(tmpAabb); - const auto tmpWorld = hlsl::float32_t3x4(promotedWorld); - const auto world4x4 = float32_t4x4{ - tmpWorld[0], - tmpWorld[1], - tmpWorld[2], - float32_t4(0, 0, 0, 1) - }; - aabbInst.color = { 1,1,1,1 }; - aabbInst.transform = math::linalg::promoted_mul(world4x4, aabbTransform); + m_aabbInstances.push_back(makeAABBInstance(aabb, worldTforms[i], hlsl::float32_t4(1, 1, 1, 1))); if (m_drawBBMode == DBBM_OBB) { @@ -704,10 +770,14 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) return pt; }); obbInst.color = { 0, 0, 1, 1 }; - obbInst.transform = math::linalg::promoted_mul(world4x4, obb.transform); + obbInst.transform = math::linalg::promoted_mul(makeAffine4x4(worldTforms[i]), obb.transform); } #endif } +#ifdef NBL_BUILD_DEBUG_DRAW + for (const auto& sceneAABB : sceneDebugAABBs) + m_aabbInstances.push_back(makeAABBInstance(sceneAABB.aabb, sceneAABB.world, hlsl::float32_t4(1, 0.65f, 0, 1))); +#endif printAABB(bound, "Total"); for (uint32_t i = 0; i < worldTforms.size(); i++) @@ -808,8 +878,7 @@ void MeshLoadersApp::setupCameraFromAABB(const hlsl::shapes::AABB<3, double>& bo const double halfZ = std::max(halfExtent.z, 0.001); const double safeRadius = std::max({ halfX, halfY, halfZ }); - // Keep startup camera horizontal and in front of the scene. - const hlsl::float64_t3 dir(0.0, 0.0, 1.0); + const hlsl::float64_t3 dir(0.0, 0.0, -1.0); const double planeHalfX = halfX; const double planeHalfY = halfY; const double depthHalf = halfZ; diff --git a/12_MeshLoaders/README.md b/12_MeshLoaders/README.md index d5b489ab3..b046bcf91 100644 --- a/12_MeshLoaders/README.md +++ b/12_MeshLoaders/README.md @@ -13,16 +13,16 @@ Example for loading and writing `OBJ`, `PLY` and `STL` meshes. ## Mode cheat sheet - `batch` - Uses test list and runs normal workflow. - - If test list has `row_view: true`, geometry assets are laid out in one inspection scene. + - If test list has `row_view: true`, assets are laid out in one inspection scene. - `interactive` - Opens file dialog and loads one model. - `ci` - Runs strict pass/fail validation per case. ## Row view concept -- `row_view` means one inspection scene containing all geometry cases from the test list. +- `row_view` means one inspection scene containing all cases from the test list. - `geometry` and `geometry collection` assets are normalized and laid out left-to-right so camera framing is stable for comparisons. -- `scene` assets keep their authored instance transforms and are rendered as scenes rather than being rewritten into row layout. +- `scene` assets are laid out as one row tile while keeping their authored internal instance transforms. ## Common workflows - Quick visual check: @@ -75,7 +75,7 @@ Example for loading and writing `OBJ`, `PLY` and `STL` meshes. - force output path - `--row-add ` - add model to row view at startup - - scene assets added this way still keep authored transforms + - scene assets added this way keep their internal transforms inside one row tile - `--row-duplicate ` - duplicate last row-view case - `--loader-perf-log ` @@ -111,7 +111,7 @@ Rules: - case item can be string path or object with `path` and optional `name` - relative paths resolve against JSON file directory - default startup uses `inputs.json` resolved from the example directory layout rather than the process working directory -- `row_view: true` affects geometry assets only. Scene assets keep their authored transforms. +- `row_view: true` keeps geometry assets in direct row layout and places each scene asset as one row tile with authored internal transforms preserved inside that tile. ## What CI validates - Per-case image consistency: From db245df40685e30c5ea0e958fcc104be837aa54a Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Tue, 10 Mar 2026 18:14:38 +0100 Subject: [PATCH 23/34] Pin mesh loaders benchmark dataset --- 12_MeshLoaders/CMakeLists.txt | 56 ++++++++++++++++------------------- 1 file changed, 25 insertions(+), 31 deletions(-) diff --git a/12_MeshLoaders/CMakeLists.txt b/12_MeshLoaders/CMakeLists.txt index 1e331fb46..778437876 100644 --- a/12_MeshLoaders/CMakeLists.txt +++ b/12_MeshLoaders/CMakeLists.txt @@ -46,41 +46,35 @@ add_dependencies(${EXECUTABLE_NAME} nlohmann_json::nlohmann_json) target_include_directories(${EXECUTABLE_NAME} PUBLIC $) if (NBL_MESHLOADERS_ENABLE_BENCHMARK_DATASETS) - set(NBL_MESHLOADERS_MEDIA_INPUTS_JSON "${CMAKE_CURRENT_SOURCE_DIR}/../media/${NBL_MESHLOADERS_MEDIA_PAYLOAD_RELATIVE_PATH}") - if (EXISTS "${NBL_MESHLOADERS_MEDIA_INPUTS_JSON}") - set(NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON "${NBL_MESHLOADERS_MEDIA_INPUTS_JSON}" CACHE FILEPATH "Committed benchmark testlist for 12_MeshLoaders." FORCE) - message(STATUS "[meshloaders-bench] Using examples media payload: ${NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON}") - else() - set(NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON "${NBL_MESHLOADERS_BENCHMARK_DATASET_DIR}/${NBL_MESHLOADERS_BENCHMARK_PAYLOAD_RELATIVE_PATH}" CACHE FILEPATH "Committed benchmark testlist for 12_MeshLoaders." FORCE) - if (NOT EXISTS "${NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON}") - if (EXISTS "${NBL_MESHLOADERS_BENCHMARK_DATASET_DIR}" AND NOT EXISTS "${NBL_MESHLOADERS_BENCHMARK_DATASET_DIR}/.git") - message(STATUS "[meshloaders-bench] Dataset dir exists without .git, skipping fetch and using local files: ${NBL_MESHLOADERS_BENCHMARK_DATASET_DIR}") - else() - include(FetchContent) - FetchContent_Declare(nbl_meshloaders_benchmark_dataset - GIT_REPOSITORY "${NBL_MESHLOADERS_BENCHMARK_DATASET_REPO}" - GIT_TAG "master" - GIT_SHALLOW TRUE - GIT_PROGRESS TRUE - SOURCE_DIR "${NBL_MESHLOADERS_BENCHMARK_DATASET_DIR}" - BINARY_DIR "${CMAKE_BINARY_DIR}/CMakeFiles/nbl_meshloaders_benchmark_dataset-build" - ) - FetchContent_GetProperties(nbl_meshloaders_benchmark_dataset) - if (NOT nbl_meshloaders_benchmark_dataset_POPULATED) - if (POLICY CMP0169) - cmake_policy(PUSH) - cmake_policy(SET CMP0169 OLD) - endif() - FetchContent_Populate(nbl_meshloaders_benchmark_dataset) - if (POLICY CMP0169) - cmake_policy(POP) - endif() - endif () + set(NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON "${NBL_MESHLOADERS_BENCHMARK_DATASET_DIR}/${NBL_MESHLOADERS_BENCHMARK_PAYLOAD_RELATIVE_PATH}" CACHE FILEPATH "Committed benchmark testlist for 12_MeshLoaders." FORCE) + if (NOT EXISTS "${NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON}") + if (EXISTS "${NBL_MESHLOADERS_BENCHMARK_DATASET_DIR}" AND NOT EXISTS "${NBL_MESHLOADERS_BENCHMARK_DATASET_DIR}/.git") + message(STATUS "[meshloaders-bench] Dataset dir exists without .git, skipping fetch and using local files: ${NBL_MESHLOADERS_BENCHMARK_DATASET_DIR}") + else() + include(FetchContent) + FetchContent_Declare(nbl_meshloaders_benchmark_dataset + GIT_REPOSITORY "${NBL_MESHLOADERS_BENCHMARK_DATASET_REPO}" + GIT_TAG "4e8af0a9b706aca95ae16f04733f5c2bed6fdb9d" + GIT_SHALLOW TRUE + GIT_PROGRESS TRUE + SOURCE_DIR "${NBL_MESHLOADERS_BENCHMARK_DATASET_DIR}" + BINARY_DIR "${CMAKE_BINARY_DIR}/CMakeFiles/nbl_meshloaders_benchmark_dataset-build" + ) + FetchContent_GetProperties(nbl_meshloaders_benchmark_dataset) + if (NOT nbl_meshloaders_benchmark_dataset_POPULATED) + if (POLICY CMP0169) + cmake_policy(PUSH) + cmake_policy(SET CMP0169 OLD) + endif() + FetchContent_Populate(nbl_meshloaders_benchmark_dataset) + if (POLICY CMP0169) + cmake_policy(POP) + endif() endif () endif () endif () if (NOT EXISTS "${NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON}") - message(FATAL_ERROR "Benchmark payload JSON not found: ${NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON}. Commit a full payload file into dataset repo and point NBL_MESHLOADERS_BENCHMARK_PAYLOAD_RELATIVE_PATH to it.") + message(FATAL_ERROR "Benchmark payload JSON not found: ${NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON}. Check NBL_MESHLOADERS_BENCHMARK_DATASET_DIR and NBL_MESHLOADERS_BENCHMARK_PAYLOAD_RELATIVE_PATH.") endif () file(READ "${NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON}" _meshloaders_payload_probe LIMIT 256) string(FIND "${_meshloaders_payload_probe}" "version https://git-lfs.github.com/spec/v1" _meshloaders_payload_lfs_ix) From 9fe02df82cc2a63153f634bb6f45c6289d8389f3 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Tue, 10 Mar 2026 19:50:04 +0100 Subject: [PATCH 24/34] Trim mesh loaders example glue --- 12_MeshLoaders/App.hpp | 12 - 12_MeshLoaders/AppLifecycle.cpp | 256 +++++++++---- 12_MeshLoaders/AppLoad.cpp | 517 ++++++++++++--------------- 12_MeshLoaders/AppRuntime.cpp | 136 +------ 12_MeshLoaders/BundleGeometryItems.h | 57 +++ 12_MeshLoaders/CMakeLists.txt | 1 + 6 files changed, 478 insertions(+), 501 deletions(-) create mode 100644 12_MeshLoaders/BundleGeometryItems.h diff --git a/12_MeshLoaders/App.hpp b/12_MeshLoaders/App.hpp index ff41f0d78..f80963011 100644 --- a/12_MeshLoaders/App.hpp +++ b/12_MeshLoaders/App.hpp @@ -86,12 +86,6 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built bool layoutAsSingleTile = false; }; - struct LoadedGeometryInstance - { - smart_refctd_ptr geometry; - hlsl::float32_t3x4 world = hlsl::math::linalg::identity(); - }; - struct RowViewPerfStats { double totalMs = 0.0; @@ -207,10 +201,6 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built static std::string normalizeExtension(const system::path& path); bool isWriteExtensionSupported(const std::string& ext) const; system::path resolveSavePath(const system::path& modelPath) const; - static system::path resolveRuntimeCWD(const system::path& preferred); - static system::path resolveDefaultTestListPath(const system::path& effectiveInputCWD, const core::vector& argv); - bool parseCommandLineOptions(const system::path& effectiveInputCWD, const system::path& effectiveOutputCWD, const system::path& defaultBenchmarkTestListPath); - std::string makeCaptionModelPath() const; bool startCase(size_t index); bool advanceToNextCase(); @@ -247,8 +237,6 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built bool validateWrittenAsset(const system::path& path); bool captureScreenshot(const system::path& path, core::smart_refctd_ptr& outImage); - bool appendGeometriesFromBundle(const asset::SAssetBundle& bundle, core::vector>& out) const; - bool appendGeometryInstancesFromBundle(const asset::SAssetBundle& bundle, core::vector& out) const; bool compareImages( const asset::ICPUImageView* a, const asset::ICPUImageView* b, diff --git a/12_MeshLoaders/AppLifecycle.cpp b/12_MeshLoaders/AppLifecycle.cpp index e93f4c3a0..99065aa5c 100644 --- a/12_MeshLoaders/AppLifecycle.cpp +++ b/12_MeshLoaders/AppLifecycle.cpp @@ -94,16 +94,40 @@ bool parseRuntimeTuningMode(const std::string_view modeRaw, asset::SFileIOPolicy return false; } -} +struct ParsedCommandLineOptions +{ + bool saveGeom = true; + bool interactive = false; + bool ci = false; + bool forceRowViewForCurrentTestList = false; + bool forceLoaderContentHashes = true; + bool updateGeometryHashReferences = false; + system::path saveGeomPrefixPath; + system::path screenshotPrefixPath; + system::path testListPath; + std::optional specifiedGeomSavePath; + std::optional loaderPerfLogPath; + std::optional rowAddPath; + uint32_t rowDuplicateCount = 0u; + asset::SFileIOPolicy::SRuntimeTuning::Mode runtimeTuningMode = asset::SFileIOPolicy::SRuntimeTuning::Mode::Heuristic; +}; + +struct CaseArtifacts +{ + std::string caseName; + system::path writtenPath; + system::path loadedScreenshotPath; + system::path writtenScreenshotPath; +}; -system::path MeshLoadersApp::resolveRuntimeCWD(const system::path& preferred) +system::path resolveRuntimeCWD(const system::path& preferred) { if (preferred.empty() || preferred == path("/") || preferred == path("\\")) return path(std::filesystem::current_path()); return preferred; } -system::path MeshLoadersApp::resolveDefaultTestListPath(const system::path& effectiveInputCWD, const core::vector& argv) +system::path resolveDefaultTestListPath(const system::path& effectiveInputCWD, const core::vector& argv) { const auto tryExisting = [](std::filesystem::path candidate) -> std::optional { @@ -133,9 +157,8 @@ system::path MeshLoadersApp::resolveDefaultTestListPath(const system::path& effe return (effectiveInputCWD / "inputs.json").lexically_normal(); } -std::string MeshLoadersApp::makeCaptionModelPath() const +std::string makeCaptionModelPath(const std::string& modelPath, const core::vector& argv) { - const auto& modelPath = m_modelPath; if (modelPath.empty()) return {}; @@ -221,24 +244,42 @@ std::string MeshLoadersApp::makeCaptionModelPath() const return targetPath.generic_string(); } -MeshLoadersApp::MeshLoadersApp( - const path& localInputCWD, - const path& localOutputCWD, - const path& sharedInputCWD, - const path& sharedOutputCWD) - : nbl::examples::MonoWindowApplication({1280, 720}, EF_D32_SFLOAT, localInputCWD, localOutputCWD, sharedInputCWD, sharedOutputCWD) - , IApplicationFramework(localInputCWD, localOutputCWD, sharedInputCWD, sharedOutputCWD) - , device_base_t({1280, 720}, EF_D32_SFLOAT, localInputCWD, localOutputCWD, sharedInputCWD, sharedOutputCWD) +template +CaseArtifacts makeCaseArtifacts( + const std::string& preferredName, + const system::path& casePath, + const system::path& screenshotPrefixPath, + ResolveSavePathFn&& resolveSavePath) { + const auto caseName = preferredName.empty() ? casePath.stem().string() : preferredName; + return { + .caseName = caseName, + .writtenPath = resolveSavePath(casePath), + .loadedScreenshotPath = screenshotPrefixPath / ("meshloaders_" + caseName + "_loaded.png"), + .writtenScreenshotPath = screenshotPrefixPath / ("meshloaders_" + caseName + "_written.png") + }; } -bool MeshLoadersApp::parseCommandLineOptions(const system::path& effectiveInputCWD, const system::path& effectiveOutputCWD, const system::path& defaultBenchmarkTestListPath) +template +bool appendRowViewDuplicates(const uint32_t duplicateCount, const system::path& lastPath, AddCaseFn&& addCase) { - m_runtime.mode = RunMode::Batch; - m_output.saveGeomPrefixPath = effectiveOutputCWD / "saved"; - m_output.screenshotPrefixPath = effectiveOutputCWD / "screenshots"; - m_output.testListPath = resolveDefaultTestListPath(effectiveInputCWD, argv); - m_runtime.forceRowViewForCurrentTestList = false; + for (uint32_t i = 0u; i < duplicateCount; ++i) + if (!addCase(lastPath)) + return false; + return true; +} + +bool parseMeshLoadersCommandLine( + const core::vector& argv, + const system::path& effectiveInputCWD, + const system::path& effectiveOutputCWD, + const system::path& defaultBenchmarkTestListPath, + ParsedCommandLineOptions& out, + std::string& error) +{ + out.saveGeomPrefixPath = effectiveOutputCWD / "saved"; + out.screenshotPrefixPath = effectiveOutputCWD / "screenshots"; + out.testListPath = resolveDefaultTestListPath(effectiveInputCWD, argv); argparse::ArgumentParser parser("12_meshloaders"); setupMeshLoadersArgumentParser(parser); @@ -249,44 +290,53 @@ bool MeshLoadersApp::parseCommandLineOptions(const system::path& effectiveInputC } catch (const std::exception& e) { - return logFail(e.what()); + error = e.what(); + return false; } if (parser["--savegeometry"] == true) - m_output.saveGeom = true; + out.saveGeom = true; if (parser["--interactive"] == true) - m_runtime.mode = RunMode::Interactive; + out.interactive = true; if (parser["--ci"] == true) - m_runtime.mode = RunMode::CI; + out.ci = true; const bool hasExplicitTestListArg = parser.present("--testlist").has_value(); if (parser.present("--savepath")) { auto tmp = path(parser.get("--savepath")); if (tmp.empty() || !tmp.has_filename()) - return logFail("Invalid path has been specified in --savepath argument"); + { + error = "Invalid path has been specified in --savepath argument"; + return false; + } if (!std::filesystem::exists(tmp.parent_path())) - return logFail("Path specified in --savepath argument doesn't exist"); - m_output.specifiedGeomSavePath.emplace(std::move(tmp.generic_string())); + { + error = "Path specified in --savepath argument doesn't exist"; + return false; + } + out.specifiedGeomSavePath.emplace(std::move(tmp.generic_string())); } if (hasExplicitTestListArg) { auto tmp = path(parser.get("--testlist")); if (tmp.empty()) - return logFail("Invalid path has been specified in --testlist argument"); + { + error = "Invalid path has been specified in --testlist argument"; + return false; + } if (tmp.is_relative()) tmp = effectiveInputCWD / tmp; - m_output.testListPath = tmp; + out.testListPath = tmp; } - else if (m_runtime.mode == RunMode::Batch && !defaultBenchmarkTestListPath.empty()) + else if (!out.interactive && !out.ci && !defaultBenchmarkTestListPath.empty()) { std::error_code benchmarkPathEc; if (std::filesystem::exists(defaultBenchmarkTestListPath, benchmarkPathEc) && !benchmarkPathEc) { - m_output.testListPath = defaultBenchmarkTestListPath; - m_runtime.forceRowViewForCurrentTestList = true; - m_logger->log("Using benchmark test list for default batch startup: %s", ILogger::ELL_INFO, m_output.testListPath.string().c_str()); + out.testListPath = defaultBenchmarkTestListPath; + out.forceRowViewForCurrentTestList = true; } } @@ -295,38 +345,57 @@ bool MeshLoadersApp::parseCommandLineOptions(const system::path& effectiveInputC auto tmp = path(parser.get("--row-add")); if (tmp.is_relative()) tmp = effectiveInputCWD / tmp; - m_output.rowAddPath = tmp; + out.rowAddPath = tmp; } if (parser.present("--row-duplicate")) { - auto countStr = parser.get("--row-duplicate"); - const auto parsedCount = parseUInt32Argument(countStr); + const auto parsedCount = parseUInt32Argument(parser.get("--row-duplicate")); if (!parsedCount.has_value()) - return logFail("Invalid --row-duplicate value."); - m_output.rowDuplicateCount = *parsedCount; + { + error = "Invalid --row-duplicate value."; + return false; + } + out.rowDuplicateCount = *parsedCount; } if (parser.present("--loader-perf-log")) { auto tmp = path(parser.get("--loader-perf-log")); if (tmp.empty()) - return logFail("Invalid --loader-perf-log value."); + { + error = "Invalid --loader-perf-log value."; + return false; + } if (tmp.is_relative()) tmp = effectiveOutputCWD / tmp; - m_output.loaderPerfLogPath = tmp; + out.loaderPerfLogPath = tmp; } if (parser["--loader-content-hashes"] == true) - m_forceLoaderContentHashes = true; + out.forceLoaderContentHashes = true; if (parser["--update-references"] == true) - m_updateGeometryHashReferences = true; + out.updateGeometryHashReferences = true; if (parser.present("--runtime-tuning")) { - auto mode = parser.get("--runtime-tuning"); - if (!parseRuntimeTuningMode(mode, m_runtimeTuningMode)) - return logFail("Invalid --runtime-tuning value. Expected: sequential|heuristic|hybrid."); + if (!parseRuntimeTuningMode(parser.get("--runtime-tuning"), out.runtimeTuningMode)) + { + error = "Invalid --runtime-tuning value. Expected: sequential|heuristic|hybrid."; + return false; + } } return true; } +} + +MeshLoadersApp::MeshLoadersApp( + const path& localInputCWD, + const path& localOutputCWD, + const path& sharedInputCWD, + const path& sharedOutputCWD) + : nbl::examples::MonoWindowApplication({1280, 720}, EF_D32_SFLOAT, localInputCWD, localOutputCWD, sharedInputCWD, sharedOutputCWD) + , IApplicationFramework(localInputCWD, localOutputCWD, sharedInputCWD, sharedOutputCWD) + , device_base_t({1280, 720}, EF_D32_SFLOAT, localInputCWD, localOutputCWD, sharedInputCWD, sharedOutputCWD) +{ +} bool MeshLoadersApp::onAppInitialized(smart_refctd_ptr&& system) { @@ -338,15 +407,38 @@ bool MeshLoadersApp::onAppInitialized(smart_refctd_ptr&& system) if (!device_base_t::onAppInitialized(smart_refctd_ptr(system))) return false; - const path effectiveInputCWD = resolveRuntimeCWD(localInputCWD); - const path effectiveOutputCWD = resolveRuntimeCWD(localOutputCWD); + const path effectiveInputCWD = ::resolveRuntimeCWD(localInputCWD); + const path effectiveOutputCWD = ::resolveRuntimeCWD(localOutputCWD); #if defined(NBL_MESHLOADERS_DEFAULT_BENCHMARK_TESTLIST_PATH) const path defaultBenchmarkTestListPath = path(NBL_MESHLOADERS_DEFAULT_BENCHMARK_TESTLIST_PATH); #else const path defaultBenchmarkTestListPath; #endif - if (!parseCommandLineOptions(effectiveInputCWD, effectiveOutputCWD, defaultBenchmarkTestListPath)) - return false; + ParsedCommandLineOptions parsed = {}; + std::string parseError; + if (!parseMeshLoadersCommandLine(argv, effectiveInputCWD, effectiveOutputCWD, defaultBenchmarkTestListPath, parsed, parseError)) + return logFail(parseError.c_str()); + auto applyParsedCommandLineOptions = [this](ParsedCommandLineOptions&& options) -> void + { + m_runtime.mode = options.interactive ? RunMode::Interactive : (options.ci ? RunMode::CI : RunMode::Batch); + m_runtime.forceRowViewForCurrentTestList = options.forceRowViewForCurrentTestList; + m_output.saveGeom = options.saveGeom; + m_output.saveGeomPrefixPath = std::move(options.saveGeomPrefixPath); + m_output.screenshotPrefixPath = std::move(options.screenshotPrefixPath); + m_output.testListPath = std::move(options.testListPath); + if (options.specifiedGeomSavePath) + m_output.specifiedGeomSavePath.emplace(std::move(*options.specifiedGeomSavePath)); + m_output.loaderPerfLogPath = std::move(options.loaderPerfLogPath); + m_output.rowAddPath = std::move(options.rowAddPath); + m_output.rowDuplicateCount = options.rowDuplicateCount; + m_forceLoaderContentHashes = options.forceLoaderContentHashes; + m_updateGeometryHashReferences = options.updateGeometryHashReferences; + m_runtimeTuningMode = options.runtimeTuningMode; + }; + applyParsedCommandLineOptions(std::move(parsed)); + + if (m_runtime.forceRowViewForCurrentTestList) + m_logger->log("Using benchmark test list for default batch startup: %s", ILogger::ELL_INFO, m_output.testListPath.string().c_str()); if (m_output.saveGeom) std::filesystem::create_directories(m_output.saveGeomPrefixPath); @@ -394,29 +486,33 @@ bool MeshLoadersApp::onAppInitialized(smart_refctd_ptr&& system) if (!initTestCases()) return false; - if (isRowViewActive()) + auto runInitialContent = [&]() -> bool { - m_runtime.nonInteractiveTest = false; - if (!loadRowView(RowViewReloadMode::Full)) - return false; - if (m_output.rowAddPath) - if (!addRowViewCaseFromPath(*m_output.rowAddPath)) - return false; - if (m_output.rowDuplicateCount > 0u && !m_runtime.cases.empty()) + if (isRowViewActive()) { - const auto lastPath = m_runtime.cases.back().path; - for (uint32_t i = 0u; i < m_output.rowDuplicateCount; ++i) - if (!addRowViewCaseFromPath(lastPath)) + m_runtime.nonInteractiveTest = false; + if (!loadRowView(RowViewReloadMode::Full)) + return false; + if (m_output.rowAddPath) + if (!addRowViewCaseFromPath(*m_output.rowAddPath)) + return false; + if (m_output.rowDuplicateCount > 0u && !m_runtime.cases.empty()) + { + const auto lastPath = m_runtime.cases.back().path; + if (!appendRowViewDuplicates(m_output.rowDuplicateCount, lastPath, [this](const system::path& path) { + return addRowViewCaseFromPath(path); + })) return false; + } + return true; } - } - else - { + if (m_runtime.mode != RunMode::Interactive) m_runtime.nonInteractiveTest = true; - if (!startCase(0u)) - return false; - } + return startCase(0u); + }; + if (!runInitialContent()) + return false; camera.mapKeysToArrows(); @@ -573,7 +669,7 @@ IQueue::SSubmitInfo::SSemaphoreInfo MeshLoadersApp::renderFrame(const std::chron std::string caption = "[Nabla Engine] Mesh Loaders"; { caption += ", displaying ["; - caption += makeCaptionModelPath(); + caption += ::makeCaptionModelPath(m_modelPath, argv); caption += "]"; m_window->setCaption(caption); } @@ -811,18 +907,28 @@ bool MeshLoadersApp::startCase(const size_t index) if (index >= m_runtime.cases.size()) return false; + auto resetCasePresentationState = [&]() -> void + { + m_runtime.phase = Phase::RenderOriginal; + m_runtime.phaseFrameCounter = 0u; + m_render.loadedScreenshot = nullptr; + m_render.writtenScreenshot = nullptr; + m_referenceCamera.reset(); + }; + m_runtime.caseIndex = index; - m_runtime.phase = Phase::RenderOriginal; - m_runtime.phaseFrameCounter = 0u; - m_render.loadedScreenshot = nullptr; - m_render.writtenScreenshot = nullptr; - m_referenceCamera.reset(); + resetCasePresentationState(); const auto& testCase = m_runtime.cases[m_runtime.caseIndex]; - m_caseName = testCase.name.empty() ? testCase.path.stem().string() : testCase.name; - m_output.writtenPath = resolveSavePath(testCase.path); - m_output.loadedScreenshotPath = m_output.screenshotPrefixPath / ("meshloaders_" + m_caseName + "_loaded.png"); - m_output.writtenScreenshotPath = m_output.screenshotPrefixPath / ("meshloaders_" + m_caseName + "_written.png"); + const auto artifacts = makeCaseArtifacts( + testCase.name, + testCase.path, + m_output.screenshotPrefixPath, + [this](const system::path& path) { return resolveSavePath(path); }); + m_caseName = artifacts.caseName; + m_output.writtenPath = artifacts.writtenPath; + m_output.loadedScreenshotPath = artifacts.loadedScreenshotPath; + m_output.writtenScreenshotPath = artifacts.writtenScreenshotPath; if (!loadModel(testCase.path, true, true)) return false; diff --git a/12_MeshLoaders/AppLoad.cpp b/12_MeshLoaders/AppLoad.cpp index d91f91925..f9bcddf7f 100644 --- a/12_MeshLoaders/AppLoad.cpp +++ b/12_MeshLoaders/AppLoad.cpp @@ -3,6 +3,7 @@ // For conditions of distribution and use, see copyright notice in nabla.h #include "App.hpp" +#include "BundleGeometryItems.h" #include #include @@ -27,6 +28,87 @@ struct RowLayoutGroup bool preserveInternalTransforms = false; bool addAggregateDebugAABB = false; }; +struct PreparedGeometryBatch +{ + core::vector> geometries; + core::vector worlds; +}; +static std::optional>> convertPolygonGeometries( + video::ILogicalDevice* device, + video::IQueue* transferQueue, + video::IQueue* graphicsQueue, + video::IUtilities* utilities, + system::ILogger* logger, + const core::vector>& geometries) +{ + if (geometries.empty()) + return core::vector>{}; + + smart_refctd_ptr converter = CAssetConverter::create({ .device = device }); + const auto transferFamily = transferQueue->getFamilyIndex(); + + struct SInputs : CAssetConverter::SInputs + { + virtual inline std::span getSharedOwnershipQueueFamilies(const size_t, const asset::ICPUBuffer*, const CAssetConverter::patch_t&) const + { + return sharedBufferOwnership; + } + + core::vector sharedBufferOwnership; + } inputs = {}; + core::vector> patches(geometries.size(), CSimpleDebugRenderer::DefaultPolygonGeometryPatch); + { + inputs.logger = logger; + std::get>(inputs.assets) = { &geometries.front().get(),geometries.size() }; + std::get>(inputs.patches) = patches; + core::unordered_set families; + families.insert(transferFamily); + families.insert(graphicsQueue->getFamilyIndex()); + if (families.size() > 1) + for (const auto fam : families) + inputs.sharedBufferOwnership.push_back(fam); + } + + auto reservation = converter->reserve(inputs); + if (!reservation) + return std::nullopt; + + { + auto semaphore = device->createSemaphore(0u); + + constexpr auto MultiBuffering = 2; + std::array, MultiBuffering> commandBuffers = {}; + { + auto pool = device->createCommandPool(transferFamily, IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT | IGPUCommandPool::CREATE_FLAGS::TRANSIENT_BIT); + pool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, commandBuffers, core::smart_refctd_ptr(logger)); + } + commandBuffers.front()->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); + + std::array commandBufferSubmits; + for (auto i = 0; i < MultiBuffering; i++) + commandBufferSubmits[i].cmdbuf = commandBuffers[i].get(); + + SIntendedSubmitInfo transfer = {}; + transfer.queue = transferQueue; + transfer.scratchCommandBuffers = commandBufferSubmits; + transfer.scratchSemaphore = { + .semaphore = semaphore.get(), + .value = 0u, + .stageMask = PIPELINE_STAGE_FLAGS::ALL_TRANSFER_BITS + }; + + CAssetConverter::SConvertParams cpar = {}; + cpar.utilities = utilities; + cpar.transfer = &transfer; + + auto future = reservation.convert(cpar); + if (future.copy() != IQueue::RESULT::SUCCESS) + return std::nullopt; + } + + const auto convertedSpan = reservation.getGPUObjects(); + return core::vector>(convertedSpan.begin(), convertedSpan.end()); +} static hlsl::float32_t3x4 makeIdentityWorld() { auto tmp = hlsl::float32_t4x3( @@ -58,6 +140,23 @@ static ext::debug_draw::InstanceData makeAABBInstance( ext::debug_draw::DrawAABB::getTransformFromAABB(shapes::AABB<3, float>(aabb.minVx, aabb.maxVx))); return instance; } +static ext::debug_draw::InstanceData makeOBBInstance( + const ICPUPolygonGeometry* geometry, + const hlsl::float32_t3x4& world, + const hlsl::float32_t4& color) +{ + ext::debug_draw::InstanceData instance = {}; + instance.color = color; + const auto obb = CPolygonGeometryManipulator::calculateOBB( + geometry->getPositionView().getElementCount(), + [geometry](size_t vertex_i) { + hlsl::float32_t3 pt; + geometry->getPositionView().decodeElement(vertex_i, pt); + return pt; + }); + instance.transform = math::linalg::promoted_mul(makeAffine4x4(world), obb.transform); + return instance; +} #endif static DisplayLayout buildDisplayLayout(const core::vector& aabbs, const bool arrangeInRow) { @@ -130,6 +229,40 @@ static DisplayLayout buildDisplayLayout(const core::vector& aabb } return retval; } +static bool extractPreparedGeometryBatch(const asset::SAssetBundle& bundle, PreparedGeometryBatch& out, const bool preserveTransforms) +{ + core::vector items; + if (!meshloaders::collectBundleGeometryItems(bundle, items, preserveTransforms)) + return false; + out.geometries.reserve(items.size()); + out.worlds.reserve(items.size()); + for (auto& item : items) + { + out.worlds.push_back(item.world); + out.geometries.push_back(std::move(item.geometry)); + } + return !out.geometries.empty(); +} +template +static void collectGeometryAABBs( + const core::vector>& geometries, + core::vector& out, + GetAABB&& getAABB, + WarnInvalid&& warnInvalid) +{ + out.clear(); + out.reserve(geometries.size()); + for (uint32_t i = 0u; i < geometries.size(); ++i) + { + auto aabb = getAABB(geometries[i].get()); + if (!nbl::examples::geometry::isValidAABB(aabb)) + { + warnInvalid(i); + aabb = nbl::examples::geometry::fallbackUnitAABB(); + } + out.push_back(aabb); + } +} } bool MeshLoadersApp::loadModel(const system::path& modelPath, bool updateCamera, bool storeCamera) @@ -165,22 +298,13 @@ bool MeshLoadersApp::loadModel(const system::path& modelPath, bool updateCamera, failExit("Failed to load asset %s.", m_modelPath.c_str()); m_render.currentCpuAsset = (asset.getContents().size() == 1u) ? asset.getContents()[0] : nullptr; - core::vector> geometries; - core::vector geometryInstances; + PreparedGeometryBatch batch = {}; const auto extractStart = clock_t::now(); const bool renderAsScene = asset.getAssetType() == IAsset::E_TYPE::ET_SCENE; - if (renderAsScene) - { - if (!appendGeometryInstancesFromBundle(asset, geometryInstances)) - failExit("Asset loaded but not a supported type for %s.", m_modelPath.c_str()); - geometries.reserve(geometryInstances.size()); - for (const auto& instance : geometryInstances) - geometries.push_back(instance.geometry); - } - else if (!appendGeometriesFromBundle(asset, geometries)) + if (!extractPreparedGeometryBatch(asset, batch, renderAsScene)) failExit("Asset loaded but not a supported type for %s.", m_modelPath.c_str()); const auto extractMs = toMs(clock_t::now() - extractStart); - if (geometries.empty()) + if (batch.geometries.empty()) failExit("No geometry found in asset %s.", m_modelPath.c_str()); const auto outerMs = toMs(clock_t::now() - loadOuterStart); @@ -194,7 +318,7 @@ bool MeshLoadersApp::loadModel(const system::path& modelPath, bool updateCamera, outerMs, nonLoaderMs); - m_render.currentCpuGeom = geometries[0]; + m_render.currentCpuGeom = batch.geometries[0]; using aabb_t = hlsl::shapes::AABB<3, double>; auto printAABB = [&](const aabb_t& aabb, const char* extraMsg = "")->void @@ -202,112 +326,49 @@ bool MeshLoadersApp::loadModel(const system::path& modelPath, bool updateCamera, m_logger->log("%s AABB is (%f,%f,%f) -> (%f,%f,%f)", ILogger::ELL_INFO, extraMsg, aabb.minVx.x, aabb.minVx.y, aabb.minVx.z, aabb.maxVx.x, aabb.maxVx.y, aabb.maxVx.z); }; core::vector aabbs; - aabbs.reserve(geometries.size()); - for (uint32_t i = 0u; i < geometries.size(); ++i) - { - auto aabb = getGeometryAABB(geometries[i].get()); - if (!isValidAABB(aabb)) - { - m_logger->log("Invalid geometry AABB for %s (geo=%u). Using fallback unit AABB for framing.", ILogger::ELL_WARNING, m_modelPath.c_str(), i); - aabb = nbl::examples::geometry::fallbackUnitAABB(); - } - aabbs.push_back(aabb); - } + collectGeometryAABBs( + batch.geometries, + aabbs, + [this](const ICPUPolygonGeometry* geometry) { return getGeometryAABB(geometry); }, + [this](const uint32_t geoIx) { + m_logger->log("Invalid geometry AABB for %s (geo=%u). Using fallback unit AABB for framing.", ILogger::ELL_WARNING, m_modelPath.c_str(), geoIx); + }); core::vector worldTforms; - worldTforms.reserve(geometries.size()); + worldTforms.reserve(batch.geometries.size()); auto bound = display_aabb_t::create(); if (renderAsScene) { - for (uint32_t i = 0u; i < geometryInstances.size(); ++i) + for (uint32_t i = 0u; i < batch.worlds.size(); ++i) { - const auto& world = geometryInstances[i].world; + const auto& world = batch.worlds[i]; worldTforms.push_back(world); bound = hlsl::shapes::util::union_(nbl::hlsl::math::linalg::pseudo_mul(hlsl::float64_t3x4(world), aabbs[i]), bound); } } else { - const auto layout = buildDisplayLayout(aabbs, geometries.size() > 1u); + const auto layout = buildDisplayLayout(aabbs, batch.geometries.size() > 1u); worldTforms = layout.worldTransforms; bound = layout.bound; } // convert the geometries { - smart_refctd_ptr converter = CAssetConverter::create({ .device = m_device.get() }); - - const auto transferFamily = getTransferUpQueue()->getFamilyIndex(); - - struct SInputs : CAssetConverter::SInputs - { - virtual inline std::span getSharedOwnershipQueueFamilies(const size_t groupCopyID, const asset::ICPUBuffer* buffer, const CAssetConverter::patch_t& patch) const - { - return sharedBufferOwnership; - } - - core::vector sharedBufferOwnership; - } inputs = {}; - core::vector> patches(geometries.size(), CSimpleDebugRenderer::DefaultPolygonGeometryPatch); - { - inputs.logger = m_logger.get(); - std::get>(inputs.assets) = { &geometries.front().get(),geometries.size() }; - std::get>(inputs.patches) = patches; - // set up shared ownership so we don't have to - core::unordered_set families; - families.insert(transferFamily); - families.insert(getGraphicsQueue()->getFamilyIndex()); - if (families.size() > 1) - for (const auto fam : families) - inputs.sharedBufferOwnership.push_back(fam); - } - - // reserve - auto reservation = converter->reserve(inputs); - if (!reservation) - { - failExit("Failed to reserve GPU objects for CPU->GPU conversion."); - } - - // convert - { - auto semaphore = m_device->createSemaphore(0u); - - constexpr auto MultiBuffering = 2; - std::array, MultiBuffering> commandBuffers = {}; - { - auto pool = m_device->createCommandPool(transferFamily, IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT | IGPUCommandPool::CREATE_FLAGS::TRANSIENT_BIT); - pool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, commandBuffers, smart_refctd_ptr(m_logger)); - } - commandBuffers.front()->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - - std::array commandBufferSubmits; - for (auto i = 0; i < MultiBuffering; i++) - commandBufferSubmits[i].cmdbuf = commandBuffers[i].get(); - - SIntendedSubmitInfo transfer = {}; - transfer.queue = getTransferUpQueue(); - transfer.scratchCommandBuffers = commandBufferSubmits; - transfer.scratchSemaphore = { - .semaphore = semaphore.get(), - .value = 0u, - .stageMask = PIPELINE_STAGE_FLAGS::ALL_TRANSFER_BITS - }; - - CAssetConverter::SConvertParams cpar = {}; - cpar.utilities = m_utils.get(); - cpar.transfer = &transfer; - - auto future = reservation.convert(cpar); - if (future.copy() != IQueue::RESULT::SUCCESS) - failExit("Failed to await submission feature."); - } - - const auto& converted = reservation.getGPUObjects(); - m_aabbInstances.resize(converted.size()); + const auto converted = convertPolygonGeometries( + m_device.get(), + getTransferUpQueue(), + getGraphicsQueue(), + m_utils.get(), + m_logger.get(), + batch.geometries); + if (!converted.has_value()) + failExit("Failed to convert CPU geometries to GPU."); + const auto& convertedGeometries = *converted; + m_aabbInstances.resize(convertedGeometries.size()); if (m_drawBBMode == DBBM_OBB) - m_obbInstances.resize(converted.size()); - for (uint32_t i = 0; i < converted.size(); i++) + m_obbInstances.resize(convertedGeometries.size()); + for (uint32_t i = 0; i < convertedGeometries.size(); i++) { - const auto& cpuGeom = geometries[i].get(); + const auto& cpuGeom = batch.geometries[i].get(); const auto& promoted = aabbs[i]; printAABB(promoted, "Geometry"); const auto promotedWorld = hlsl::float64_t3x4(worldTforms[i]); @@ -315,39 +376,17 @@ bool MeshLoadersApp::loadModel(const system::path& modelPath, bool updateCamera, printAABB(transformed, "Transformed"); #ifdef NBL_BUILD_DEBUG_DRAW - auto& aabbInst = m_aabbInstances[i]; - const auto tmpAabb = shapes::AABB<3, float>(promoted.minVx, promoted.maxVx); - - hlsl::float32_t3x4 aabbTransform = ext::debug_draw::DrawAABB::getTransformFromAABB(tmpAabb); - const auto tmpWorld = hlsl::float32_t3x4(promotedWorld); - const auto world4x4 = float32_t4x4{ - tmpWorld[0], - tmpWorld[1], - tmpWorld[2], - float32_t4(0, 0, 0, 1) - }; - - aabbInst.color = { 1, 1, 1, 1 }; - aabbInst.transform = math::linalg::promoted_mul(world4x4, aabbTransform); + m_aabbInstances[i] = makeAABBInstance(promoted, worldTforms[i], hlsl::float32_t4(1, 1, 1, 1)); if (m_drawBBMode == DBBM_OBB) { - auto& obbInst = m_obbInstances[i]; - const auto obb = CPolygonGeometryManipulator::calculateOBB( - cpuGeom->getPositionView().getElementCount(), - [geo = cpuGeom, &world4x4](size_t vertex_i) { - hlsl::float32_t3 pt; - geo->getPositionView().decodeElement(vertex_i, pt); - return pt; - }); - obbInst.color = { 0, 0, 1, 1 }; - obbInst.transform = math::linalg::promoted_mul(world4x4, obb.transform); + m_obbInstances[i] = makeOBBInstance(cpuGeom, worldTforms[i], hlsl::float32_t4(0, 0, 1, 1)); } #endif } printAABB(bound, "Total"); - if (!m_render.renderer->addGeometries({ &converted.front().get(),converted.size() })) + if (!m_render.renderer->addGeometries({ &convertedGeometries.front().get(),convertedGeometries.size() })) failExit("Failed to add geometries to renderer."); if (m_logger) { @@ -434,6 +473,58 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) entry.tileAABB = nbl::examples::geometry::fallbackUnitAABB(); } }; + auto assignCachedEntryGeometryBatch = [&](CachedGeometryEntry& entry, PreparedGeometryBatch&& batch) -> void + { + entry.cpu = std::move(batch.geometries); + entry.world = std::move(batch.worlds); + if (!entry.layoutAsSingleTile) + entry.world.assign(entry.cpu.size(), hlsl::math::linalg::identity()); + entry.gpu.resize(entry.cpu.size()); + }; + auto refreshCachedEntryAABBs = [&](CachedGeometryEntry& entry, const system::path& path) -> void + { + collectGeometryAABBs( + entry.cpu, + entry.aabbs, + [this](const ICPUPolygonGeometry* geometry) { return getGeometryAABB(geometry); }, + [this, &path](const uint32_t geoIx) { + m_logger->log("Invalid row-view geometry AABB for %s (geo=%u). Using fallback unit AABB.", ILogger::ELL_WARNING, path.string().c_str(), geoIx); + }); + rebuildTileAABB(entry, path); + }; + auto appendCachedEntryToLayoutInputs = [&]( + const CachedGeometryEntry& entry, + core::vector>& geometryOut, + core::vector>& aabbOut, + core::vector& worldOut, + core::vector& groupOut) -> void + { + const size_t firstGeometry = geometryOut.size(); + geometryOut.insert(geometryOut.end(), entry.cpu.begin(), entry.cpu.end()); + aabbOut.insert(aabbOut.end(), entry.aabbs.begin(), entry.aabbs.end()); + worldOut.insert(worldOut.end(), entry.world.begin(), entry.world.end()); + if (entry.layoutAsSingleTile) + { + groupOut.push_back({ + .firstGeometry = firstGeometry, + .geometryCount = entry.cpu.size(), + .layoutAABB = entry.tileAABB, + .preserveInternalTransforms = true, + .addAggregateDebugAABB = true + }); + return; + } + for (size_t geoIx = 0u; geoIx < entry.cpu.size(); ++geoIx) + { + groupOut.push_back({ + .firstGeometry = firstGeometry + geoIx, + .geometryCount = 1u, + .layoutAABB = entry.aabbs[geoIx], + .preserveInternalTransforms = false, + .addAggregateDebugAABB = false + }); + } + }; for (const auto& testCase : m_runtime.cases) { @@ -461,48 +552,15 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) const auto extractStart = clock_t::now(); entry.world.clear(); entry.layoutAsSingleTile = (asset.getAssetType() == IAsset::E_TYPE::ET_SCENE); - if (asset.getAssetType() == IAsset::E_TYPE::ET_SCENE) - { - core::vector found; - if (appendGeometryInstancesFromBundle(asset, found)) - { - entry.cpu.reserve(found.size()); - entry.world.reserve(found.size()); - for (auto& instance : found) - { - entry.cpu.push_back(instance.geometry); - entry.world.push_back(instance.world); - } - } - } - else - { - core::vector> found; - if (appendGeometriesFromBundle(asset, found)) - { - entry.cpu = std::move(found); - entry.world.assign(entry.cpu.size(), hlsl::math::linalg::identity()); - } - } + PreparedGeometryBatch batch = {}; + if (extractPreparedGeometryBatch(asset, batch, entry.layoutAsSingleTile)) + assignCachedEntryGeometryBatch(entry, std::move(batch)); stats.extractMs += toMs(clock_t::now() - extractStart); if (entry.cpu.empty()) failExit("No geometry found in asset %s.", path.string().c_str()); - entry.gpu.resize(entry.cpu.size()); const auto aabbStart = clock_t::now(); - entry.aabbs.clear(); - entry.aabbs.reserve(entry.cpu.size()); - for (uint32_t geoIx = 0u; geoIx < entry.cpu.size(); ++geoIx) - { - auto aabb = getGeometryAABB(entry.cpu[geoIx].get()); - if (!isValidAABB(aabb)) - { - m_logger->log("Invalid row-view geometry AABB for %s (geo=%u). Using fallback unit AABB.", ILogger::ELL_WARNING, path.string().c_str(), geoIx); - aabb = nbl::examples::geometry::fallbackUnitAABB(); - } - entry.aabbs.push_back(aabb); - } - rebuildTileAABB(entry, path); + refreshCachedEntryAABBs(entry, path); stats.aabbMs += toMs(clock_t::now() - aabbStart); } else @@ -515,21 +573,9 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) if (entry.aabbs.size() != entry.cpu.size()) { const auto aabbStart = clock_t::now(); - entry.aabbs.clear(); - entry.aabbs.reserve(entry.cpu.size()); - for (uint32_t geoIx = 0u; geoIx < entry.cpu.size(); ++geoIx) - { - auto aabb = getGeometryAABB(entry.cpu[geoIx].get()); - if (!isValidAABB(aabb)) - { - m_logger->log("Invalid row-view geometry AABB for %s (geo=%u). Using fallback unit AABB.", ILogger::ELL_WARNING, path.string().c_str(), geoIx); - aabb = nbl::examples::geometry::fallbackUnitAABB(); - } - entry.aabbs.push_back(aabb); - } + refreshCachedEntryAABBs(entry, path); stats.aabbMs += toMs(clock_t::now() - aabbStart); } - rebuildTileAABB(entry, path); } logRowViewAssetLoad(path, assetLoadMs, cached); @@ -545,33 +591,7 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) stats.gpuHits++; } - const size_t firstGeometry = geometries.size(); - geometries.insert(geometries.end(), entry.cpu.begin(), entry.cpu.end()); - aabbs.insert(aabbs.end(), entry.aabbs.begin(), entry.aabbs.end()); - sourceWorlds.insert(sourceWorlds.end(), entry.world.begin(), entry.world.end()); - if (entry.layoutAsSingleTile) - { - layoutGroups.push_back({ - .firstGeometry = firstGeometry, - .geometryCount = entry.cpu.size(), - .layoutAABB = entry.tileAABB, - .preserveInternalTransforms = true, - .addAggregateDebugAABB = true - }); - } - else - { - for (size_t geoIx = 0u; geoIx < entry.cpu.size(); ++geoIx) - { - layoutGroups.push_back({ - .firstGeometry = firstGeometry + geoIx, - .geometryCount = 1u, - .layoutAABB = entry.aabbs[geoIx], - .preserveInternalTransforms = false, - .addAggregateDebugAABB = false - }); - } - } + appendCachedEntryToLayoutInputs(entry, geometries, aabbs, sourceWorlds, layoutGroups); } if (geometries.empty()) @@ -582,72 +602,18 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) { stats.convertCount = cpuToConvert.size(); const auto convertStart = clock_t::now(); - - smart_refctd_ptr converter = CAssetConverter::create({ .device = m_device.get() }); - const auto transferFamily = getTransferUpQueue()->getFamilyIndex(); - - struct SInputs : CAssetConverter::SInputs - { - virtual inline std::span getSharedOwnershipQueueFamilies(const size_t, const asset::ICPUBuffer*, const CAssetConverter::patch_t&) const - { - return sharedBufferOwnership; - } - - core::vector sharedBufferOwnership; - } inputs = {}; - core::vector> patches(cpuToConvert.size(), CSimpleDebugRenderer::DefaultPolygonGeometryPatch); - { - inputs.logger = m_logger.get(); - std::get>(inputs.assets) = { &cpuToConvert.front().get(),cpuToConvert.size() }; - std::get>(inputs.patches) = patches; - core::unordered_set families; - families.insert(transferFamily); - families.insert(getGraphicsQueue()->getFamilyIndex()); - if (families.size() > 1) - for (const auto fam : families) - inputs.sharedBufferOwnership.push_back(fam); - } - - auto reservation = converter->reserve(inputs); - if (!reservation) - failExit("Failed to reserve GPU objects for CPU->GPU conversion."); - - { - auto semaphore = m_device->createSemaphore(0u); - - constexpr auto MultiBuffering = 2; - std::array, MultiBuffering> commandBuffers = {}; - { - auto pool = m_device->createCommandPool(transferFamily, IGPUCommandPool::CREATE_FLAGS::RESET_COMMAND_BUFFER_BIT | IGPUCommandPool::CREATE_FLAGS::TRANSIENT_BIT); - pool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, commandBuffers, smart_refctd_ptr(m_logger)); - } - commandBuffers.front()->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); - - std::array commandBufferSubmits; - for (auto i = 0; i < MultiBuffering; i++) - commandBufferSubmits[i].cmdbuf = commandBuffers[i].get(); - - SIntendedSubmitInfo transfer = {}; - transfer.queue = getTransferUpQueue(); - transfer.scratchCommandBuffers = commandBufferSubmits; - transfer.scratchSemaphore = { - .semaphore = semaphore.get(), - .value = 0u, - .stageMask = PIPELINE_STAGE_FLAGS::ALL_TRANSFER_BITS - }; - - CAssetConverter::SConvertParams cpar = {}; - cpar.utilities = m_utils.get(); - cpar.transfer = &transfer; - - auto future = reservation.convert(cpar); - if (future.copy() != IQueue::RESULT::SUCCESS) - failExit("Failed to await submission feature."); - } - - const auto& converted = reservation.getGPUObjects(); - for (size_t i = 0u; i < converted.size(); ++i) - convertTargets[i].entry->gpu[convertTargets[i].geometryIx] = converted[i]; + const auto converted = convertPolygonGeometries( + m_device.get(), + getTransferUpQueue(), + getGraphicsQueue(), + m_utils.get(), + m_logger.get(), + cpuToConvert); + if (!converted.has_value()) + failExit("Failed to convert CPU geometries to GPU."); + const auto& convertedGeometries = *converted; + for (size_t i = 0u; i < convertedGeometries.size(); ++i) + convertTargets[i].entry->gpu[convertTargets[i].geometryIx] = convertedGeometries[i]; stats.convertMs = toMs(clock_t::now() - convertStart); } @@ -760,18 +726,7 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) m_aabbInstances.push_back(makeAABBInstance(aabb, worldTforms[i], hlsl::float32_t4(1, 1, 1, 1))); if (m_drawBBMode == DBBM_OBB) - { - auto& obbInst = m_obbInstances[i]; - const auto obb = CPolygonGeometryManipulator::calculateOBB( - cpuGeom->getPositionView().getElementCount(), - [geo = cpuGeom](size_t vertex_i) { - hlsl::float32_t3 pt; - geo->getPositionView().decodeElement(vertex_i, pt); - return pt; - }); - obbInst.color = { 0, 0, 1, 1 }; - obbInst.transform = math::linalg::promoted_mul(makeAffine4x4(worldTforms[i]), obb.transform); - } + m_obbInstances[i] = makeOBBInstance(cpuGeom, worldTforms[i], hlsl::float32_t4(0, 0, 1, 1)); #endif } #ifdef NBL_BUILD_DEBUG_DRAW diff --git a/12_MeshLoaders/AppRuntime.cpp b/12_MeshLoaders/AppRuntime.cpp index 2db84d668..b00893c17 100644 --- a/12_MeshLoaders/AppRuntime.cpp +++ b/12_MeshLoaders/AppRuntime.cpp @@ -3,6 +3,7 @@ // For conditions of distribution and use, see copyright notice in nabla.h #include "App.hpp" +#include "BundleGeometryItems.h" #include "nbl/ext/ScreenShot/ScreenShot.h" #include "nbl/examples/common/ImageComparison.h" @@ -95,8 +96,8 @@ bool MeshLoadersApp::validateWrittenAsset(const system::path& path) if (asset.getContents().empty()) return false; - core::vector> geometries; - if (!appendGeometriesFromBundle(asset, geometries)) + core::vector geometries; + if (!meshloaders::collectBundleGeometryItems(asset, geometries, false)) return false; return !geometries.empty(); } @@ -138,137 +139,6 @@ bool MeshLoadersApp::captureScreenshot(const system::path& path, core::smart_ref return true; } -bool MeshLoadersApp::appendGeometriesFromBundle(const asset::SAssetBundle& bundle, core::vector>& out) const -{ - if (bundle.getContents().empty()) - return false; - - auto appendCollection = [&](const ICPUGeometryCollection* collection) -> void - { - if (!collection) - return; - const auto& refs = collection->getGeometries(); - for (const auto& ref : refs) - { - if (!ref.geometry) - continue; - if (ref.geometry->getPrimitiveType() != IGeometryBase::EPrimitiveType::Polygon) - continue; - const auto assetRef = core::smart_refctd_ptr_static_cast(ref.geometry); - auto poly = IAsset::castDown(assetRef); - if (poly) - out.push_back(poly); - } - }; - - switch (bundle.getAssetType()) - { - case IAsset::E_TYPE::ET_GEOMETRY: - for (const auto& item : bundle.getContents()) - { - if (auto polyGeo = IAsset::castDown(item); polyGeo) - out.push_back(polyGeo); - } - break; - case IAsset::E_TYPE::ET_GEOMETRY_COLLECTION: - for (const auto& item : bundle.getContents()) - { - auto collection = IAsset::castDown(item); - appendCollection(collection.get()); - } - break; - case IAsset::E_TYPE::ET_SCENE: - for (const auto& item : bundle.getContents()) - { - auto scene = IAsset::castDown(item); - if (!scene) - continue; - const auto& instances = scene->getInstances().getMorphTargets(); - for (const auto& morphTargets : instances) - { - if (!morphTargets) - continue; - const auto& targets = *morphTargets->getTargets(); - for (const auto& target : targets) - appendCollection(target.geoCollection.get()); - } - } - break; - default: - return false; - } - - return !out.empty(); -} - -bool MeshLoadersApp::appendGeometryInstancesFromBundle(const asset::SAssetBundle& bundle, core::vector& out) const -{ - if (bundle.getContents().empty()) - return false; - const auto identity = hlsl::math::linalg::identity(); - auto appendCollection = [&](const ICPUGeometryCollection* collection, const hlsl::float32_t3x4& baseTransform, const bool preserveReferenceTransforms) -> void - { - if (!collection) - return; - const auto& refs = collection->getGeometries(); - for (const auto& ref : refs) - { - if (!ref.geometry || ref.geometry->getPrimitiveType() != IGeometryBase::EPrimitiveType::Polygon) - continue; - const auto assetRef = core::smart_refctd_ptr_static_cast(ref.geometry); - auto poly = IAsset::castDown(assetRef); - if (!poly) - continue; - LoadedGeometryInstance instance = {.geometry = std::move(poly),.world = baseTransform}; - if (preserveReferenceTransforms && ref.hasTransform()) - instance.world = hlsl::math::linalg::promoted_mul(baseTransform, ref.transform); - out.push_back(std::move(instance)); - } - }; - switch (bundle.getAssetType()) - { - case IAsset::E_TYPE::ET_GEOMETRY: - for (const auto& item : bundle.getContents()) - { - auto polyGeo = IAsset::castDown(item); - if (polyGeo) - out.push_back({.geometry = std::move(polyGeo),.world = identity}); - } - break; - case IAsset::E_TYPE::ET_GEOMETRY_COLLECTION: - for (const auto& item : bundle.getContents()) - { - auto collection = IAsset::castDown(item); - appendCollection(collection.get(), identity, false); - } - break; - case IAsset::E_TYPE::ET_SCENE: - for (const auto& item : bundle.getContents()) - { - auto scene = IAsset::castDown(item); - if (!scene) - continue; - const auto& instances = scene->getInstances(); - const auto& morphTargets = instances.getMorphTargets(); - const auto& initialTransforms = instances.getInitialTransforms(); - for (uint32_t instanceIx = 0u; instanceIx < morphTargets.size(); ++instanceIx) - { - const auto* targets = morphTargets[instanceIx].get(); - if (!targets) - continue; - const auto instanceTransform = initialTransforms.empty() ? identity : initialTransforms[instanceIx]; - const auto& targetList = targets->getTargets(); - for (const auto& target : targetList) - appendCollection(target.geoCollection.get(), instanceTransform, true); - } - } - break; - default: - return false; - } - return !out.empty(); -} - bool MeshLoadersApp::compareImages( const asset::ICPUImageView* a, const asset::ICPUImageView* b, diff --git a/12_MeshLoaders/BundleGeometryItems.h b/12_MeshLoaders/BundleGeometryItems.h new file mode 100644 index 000000000..e0b801af4 --- /dev/null +++ b/12_MeshLoaders/BundleGeometryItems.h @@ -0,0 +1,57 @@ +#ifndef _NBL_EXAMPLES_12_MESHLOADERS_BUNDLE_GEOMETRY_ITEMS_H_INCLUDED_ +#define _NBL_EXAMPLES_12_MESHLOADERS_BUNDLE_GEOMETRY_ITEMS_H_INCLUDED_ + +#include "include/common.hpp" + +#include "nbl/asset/interchange/SGeometryWriterCommon.h" + +namespace meshloaders +{ + +struct BundleGeometryItem +{ + core::smart_refctd_ptr geometry; + hlsl::float32_t3x4 world = hlsl::math::linalg::identity(); +}; + +inline bool collectBundleGeometryItems( + const asset::SAssetBundle& bundle, + core::vector& out, + const bool preserveTransforms) +{ + if (bundle.getContents().empty()) + return false; + + switch (bundle.getAssetType()) + { + case IAsset::E_TYPE::ET_GEOMETRY: + case IAsset::E_TYPE::ET_GEOMETRY_COLLECTION: + case IAsset::E_TYPE::ET_SCENE: + break; + default: + return false; + } + + const auto identity = hlsl::math::linalg::identity(); + const bool useItemTransforms = preserveTransforms && bundle.getAssetType() == IAsset::E_TYPE::ET_SCENE; + for (const auto& root : bundle.getContents()) + { + if (!root) + continue; + const auto items = asset::SGeometryWriterCommon::collectPolygonGeometryWriteItems(root.get()); + for (const auto& item : items) + { + if (!item.geometry) + continue; + out.push_back({ + .geometry = core::smart_refctd_ptr(const_cast(item.geometry)), + .world = useItemTransforms ? item.transform : identity + }); + } + } + return !out.empty(); +} + +} + +#endif diff --git a/12_MeshLoaders/CMakeLists.txt b/12_MeshLoaders/CMakeLists.txt index 778437876..515456ebb 100644 --- a/12_MeshLoaders/CMakeLists.txt +++ b/12_MeshLoaders/CMakeLists.txt @@ -1,6 +1,7 @@ set(SRCs main.cpp App.hpp + BundleGeometryItems.h AppLifecycle.cpp AppLoad.cpp AppRuntime.cpp From 9d1103fe35fda109e0c659dc2675d133b8f4b170 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Tue, 10 Mar 2026 20:46:26 +0100 Subject: [PATCH 25/34] Pipeline mesh loaders screenshot capture --- 12_MeshLoaders/App.hpp | 26 +++- 12_MeshLoaders/AppLifecycle.cpp | 22 ++- 12_MeshLoaders/AppRuntime.cpp | 268 ++++++++++++++++++++++++++++---- 3 files changed, 280 insertions(+), 36 deletions(-) diff --git a/12_MeshLoaders/App.hpp b/12_MeshLoaders/App.hpp index f80963011..276bb8c25 100644 --- a/12_MeshLoaders/App.hpp +++ b/12_MeshLoaders/App.hpp @@ -61,7 +61,9 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built enum class Phase { RenderOriginal, - RenderWritten + CaptureOriginalPending, + RenderWritten, + CaptureWrittenPending }; enum class RowViewReloadMode @@ -123,6 +125,24 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built uintmax_t inputSize = 0u; }; + struct PendingScreenshotCapture + { + nbl::system::path path; + core::smart_refctd_ptr sourceView; + core::smart_refctd_ptr commandBuffer; + core::smart_refctd_ptr texelBuffer; + core::smart_refctd_ptr completionSemaphore; + asset::IImage::SCreationParams imageParams = {}; + asset::IImage::SSubresourceRange subresourceRange = {}; + asset::E_FORMAT viewFormat = asset::EF_UNKNOWN; + uint64_t completionValue = 0u; + + inline bool active() const + { + return static_cast(completionSemaphore); + } + }; + struct RuntimeState { bool nonInteractiveTest = false; @@ -168,6 +188,7 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built core::smart_refctd_ptr currentCpuGeom; core::smart_refctd_ptr loadedScreenshot; core::smart_refctd_ptr writtenScreenshot; + PendingScreenshotCapture pendingScreenshot; }; struct RowViewState @@ -236,7 +257,8 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built void logRowViewLoadTotal(double ms, size_t hits, size_t misses) const; bool validateWrittenAsset(const system::path& path); - bool captureScreenshot(const system::path& path, core::smart_refctd_ptr& outImage); + bool requestScreenshotCapture(const system::path& path); + bool finalizeScreenshotCapture(core::smart_refctd_ptr& outImage, bool& ready); bool compareImages( const asset::ICPUImageView* a, const asset::ICPUImageView* b, diff --git a/12_MeshLoaders/AppLifecycle.cpp b/12_MeshLoaders/AppLifecycle.cpp index 99065aa5c..50e102fe7 100644 --- a/12_MeshLoaders/AppLifecycle.cpp +++ b/12_MeshLoaders/AppLifecycle.cpp @@ -673,11 +673,22 @@ IQueue::SSubmitInfo::SSemaphoreInfo MeshLoadersApp::renderFrame(const std::chron caption += "]"; m_window->setCaption(caption); } - if (isRowViewActive() && !m_runtime.rowViewScreenshotCaptured && m_render.realFrameIx >= RowViewFramesBeforeCapture) + const uint64_t rowViewCaptureRequestFrame = (RowViewFramesBeforeCapture > 1u) ? (RowViewFramesBeforeCapture - 1u) : RowViewFramesBeforeCapture; + if (isRowViewActive() && !m_runtime.rowViewScreenshotCaptured && m_render.realFrameIx >= rowViewCaptureRequestFrame) { - if (!captureScreenshot(m_output.rowViewScreenshotPath, m_render.loadedScreenshot)) - failExit("Failed to capture row view screenshot."); - m_runtime.rowViewScreenshotCaptured = true; + if (!m_render.pendingScreenshot.active()) + { + if (!requestScreenshotCapture(m_output.rowViewScreenshotPath)) + failExit("Failed to request row view screenshot capture."); + } + else + { + bool ready = false; + if (!finalizeScreenshotCapture(m_render.loadedScreenshot, ready)) + failExit("Failed to finalize row view screenshot."); + if (ready) + m_runtime.rowViewScreenshotCaptured = true; + } } advanceCase(); return retval; @@ -913,6 +924,7 @@ bool MeshLoadersApp::startCase(const size_t index) m_runtime.phaseFrameCounter = 0u; m_render.loadedScreenshot = nullptr; m_render.writtenScreenshot = nullptr; + m_render.pendingScreenshot = {}; m_referenceCamera.reset(); }; @@ -987,6 +999,7 @@ bool MeshLoadersApp::addRowViewCaseFromPath(const system::path& picked) bool MeshLoadersApp::reloadFromTestList() { m_runtime.cases.clear(); + m_render.pendingScreenshot = {}; if (!loadTestList(m_output.testListPath)) return false; m_runtime.shouldQuit = false; @@ -1005,6 +1018,7 @@ void MeshLoadersApp::resetRowViewScene() if (!isRowViewActive()) return; m_runtime.cases.clear(); + m_render.pendingScreenshot = {}; m_rowView.cache.clear(); m_render.renderer->m_instances.clear(); m_render.renderer->clearGeometries({ .semaphore = m_render.semaphore.get(),.value = m_render.realFrameIx }); diff --git a/12_MeshLoaders/AppRuntime.cpp b/12_MeshLoaders/AppRuntime.cpp index b00893c17..1a430df8e 100644 --- a/12_MeshLoaders/AppRuntime.cpp +++ b/12_MeshLoaders/AppRuntime.cpp @@ -5,10 +5,11 @@ #include "App.hpp" #include "BundleGeometryItems.h" -#include "nbl/ext/ScreenShot/ScreenShot.h" #include "nbl/examples/common/ImageComparison.h" #include "nbl/builtin/hlsl/math/linalg/fast_affine.hlsl" +#include + std::string MeshLoadersApp::makeUniqueCaseName(const system::path& path) { auto base = path.stem().string(); @@ -102,15 +103,15 @@ bool MeshLoadersApp::validateWrittenAsset(const system::path& path) return !geometries.empty(); } -bool MeshLoadersApp::captureScreenshot(const system::path& path, core::smart_refctd_ptr& outImage) +bool MeshLoadersApp::requestScreenshotCapture(const system::path& path) { if (!m_device || !m_surface || !m_assetMgr) return false; + if (m_render.pendingScreenshot.active()) + return false; - m_device->waitIdle(); - - auto* scRes = static_cast(m_surface->getSwapchainResources()); - auto* fb = scRes ? scRes->getFramebuffer(device_base_t::getCurrentAcquire().imageIndex) : nullptr; + auto* const scRes = static_cast(m_surface->getSwapchainResources()); + auto* const fb = scRes ? scRes->getFramebuffer(device_base_t::getCurrentAcquire().imageIndex) : nullptr; if (!fb) return false; @@ -118,24 +119,209 @@ bool MeshLoadersApp::captureScreenshot(const system::path& path, core::smart_ref if (!colorView) return false; - auto cpuView = ext::ScreenShot::createScreenShot( - m_device.get(), - getGraphicsQueue(), - nullptr, - colorView.get(), - asset::ACCESS_FLAGS::COLOR_ATTACHMENT_WRITE_BIT, - asset::IImage::LAYOUT::PRESENT_SRC); - if (!cpuView) + auto gpuImage = colorView->getCreationParameters().image; + if (!gpuImage) + return false; + + const auto imageParams = gpuImage->getCreationParameters(); + if (!imageParams.usage.hasFlags(asset::IImage::EUF_TRANSFER_SRC_BIT)) + return false; + if (asset::isBlockCompressionFormat(imageParams.format)) + return false; + + auto commandPool = m_device->createCommandPool(getGraphicsQueue()->getFamilyIndex(), IGPUCommandPool::CREATE_FLAGS::TRANSIENT_BIT); + if (!commandPool) + return false; + + core::smart_refctd_ptr commandBuffer; + if (!commandPool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, { &commandBuffer, 1u }) || !commandBuffer) + return false; + if (!commandBuffer->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT)) + return false; + + const auto imageViewParams = colorView->getCreationParameters(); + const auto extent = gpuImage->getMipSize(); + + IGPUImage::SBufferCopy copyRegion = {}; + copyRegion.imageSubresource.aspectMask = imageViewParams.subresourceRange.aspectMask; + copyRegion.imageSubresource.mipLevel = imageViewParams.subresourceRange.baseMipLevel; + copyRegion.imageSubresource.baseArrayLayer = imageViewParams.subresourceRange.baseArrayLayer; + copyRegion.imageSubresource.layerCount = imageViewParams.subresourceRange.layerCount; + copyRegion.imageExtent = { extent.x, extent.y, extent.z }; + + IGPUBuffer::SCreationParams bufferParams = {}; + bufferParams.size = static_cast(extent.x) * static_cast(extent.y) * static_cast(extent.z) * asset::getTexelOrBlockBytesize(imageParams.format); + bufferParams.usage = video::IGPUBuffer::EUF_TRANSFER_DST_BIT; + auto texelBuffer = m_device->createBuffer(std::move(bufferParams)); + if (!texelBuffer) + return false; + + auto texelBufferMemReqs = texelBuffer->getMemoryReqs(); + texelBufferMemReqs.memoryTypeBits &= m_device->getPhysicalDevice()->getDownStreamingMemoryTypeBits(); + if (!texelBufferMemReqs.memoryTypeBits) + return false; + auto texelBufferMemory = m_device->allocate(texelBufferMemReqs, texelBuffer.get()); + if (!texelBufferMemory.isValid()) + return false; + + IGPUCommandBuffer::SPipelineBarrierDependencyInfo dependencyInfo = {}; + IGPUCommandBuffer::SPipelineBarrierDependencyInfo::image_barrier_t imageBarrier = {}; + dependencyInfo.imgBarriers = { &imageBarrier, &imageBarrier + 1 }; + + imageBarrier.barrier.dep.srcStageMask = PIPELINE_STAGE_FLAGS::ALL_COMMANDS_BITS; + imageBarrier.barrier.dep.srcAccessMask = asset::ACCESS_FLAGS::COLOR_ATTACHMENT_WRITE_BIT; + imageBarrier.barrier.dep.dstStageMask = PIPELINE_STAGE_FLAGS::COPY_BIT; + imageBarrier.barrier.dep.dstAccessMask = asset::ACCESS_FLAGS::TRANSFER_READ_BIT; + imageBarrier.oldLayout = asset::IImage::LAYOUT::PRESENT_SRC; + imageBarrier.newLayout = asset::IImage::LAYOUT::TRANSFER_SRC_OPTIMAL; + imageBarrier.image = gpuImage.get(); + imageBarrier.subresourceRange.aspectMask = imageViewParams.subresourceRange.aspectMask; + imageBarrier.subresourceRange.baseMipLevel = imageViewParams.subresourceRange.baseMipLevel; + imageBarrier.subresourceRange.levelCount = 1u; + imageBarrier.subresourceRange.baseArrayLayer = imageViewParams.subresourceRange.baseArrayLayer; + imageBarrier.subresourceRange.layerCount = imageViewParams.subresourceRange.layerCount; + commandBuffer->pipelineBarrier(asset::EDF_NONE, dependencyInfo); + + commandBuffer->copyImageToBuffer(gpuImage.get(), asset::IImage::LAYOUT::TRANSFER_SRC_OPTIMAL, texelBuffer.get(), 1u, ©Region); + + imageBarrier.barrier.dep.srcStageMask = imageBarrier.barrier.dep.dstStageMask; + imageBarrier.barrier.dep.srcAccessMask = asset::ACCESS_FLAGS::NONE; + imageBarrier.barrier.dep.dstStageMask = PIPELINE_STAGE_FLAGS::ALL_COMMANDS_BITS; + imageBarrier.barrier.dep.dstAccessMask = asset::ACCESS_FLAGS::NONE; + imageBarrier.oldLayout = imageBarrier.newLayout; + imageBarrier.newLayout = asset::IImage::LAYOUT::PRESENT_SRC; + commandBuffer->pipelineBarrier(asset::EDF_NONE, dependencyInfo); + + if (!commandBuffer->end()) return false; - if (!path.empty()) - std::filesystem::create_directories(path.parent_path()); + auto completionSemaphore = m_device->createSemaphore(0u); + if (!completionSemaphore) + return false; + + IQueue::SSubmitInfo::SCommandBufferInfo commandBufferInfo = { .cmdbuf = commandBuffer.get() }; + IQueue::SSubmitInfo::SSemaphoreInfo waitInfo = { + .semaphore = m_render.semaphore.get(), + .value = m_render.realFrameIx, + .stageMask = PIPELINE_STAGE_FLAGS::COPY_BIT + }; + IQueue::SSubmitInfo::SSemaphoreInfo signalInfo = { + .semaphore = completionSemaphore.get(), + .value = 1u, + .stageMask = PIPELINE_STAGE_FLAGS::COPY_BIT + }; + IQueue::SSubmitInfo submitInfo = { + .waitSemaphores = { &waitInfo, 1u }, + .commandBuffers = { &commandBufferInfo, 1u }, + .signalSemaphores = { &signalInfo, 1u } + }; + if (getGraphicsQueue()->submit({ &submitInfo, 1u }) != IQueue::RESULT::SUCCESS) + return false; + + m_render.pendingScreenshot.path = path; + m_render.pendingScreenshot.sourceView = std::move(colorView); + m_render.pendingScreenshot.commandBuffer = std::move(commandBuffer); + m_render.pendingScreenshot.texelBuffer = std::move(texelBuffer); + m_render.pendingScreenshot.completionSemaphore = std::move(completionSemaphore); + m_render.pendingScreenshot.imageParams = imageParams; + m_render.pendingScreenshot.subresourceRange = imageViewParams.subresourceRange; + m_render.pendingScreenshot.viewFormat = imageViewParams.format; + m_render.pendingScreenshot.completionValue = 1u; + return true; +} + +bool MeshLoadersApp::finalizeScreenshotCapture(core::smart_refctd_ptr& outImage, bool& ready) +{ + ready = false; + if (!m_render.pendingScreenshot.active()) + return false; + + if (m_render.pendingScreenshot.completionSemaphore->getCounterValue() < m_render.pendingScreenshot.completionValue) + return true; + + const auto texelBufferSize = m_render.pendingScreenshot.texelBuffer->getSize(); + auto* const allocation = m_render.pendingScreenshot.texelBuffer->getBoundMemory().memory; + if (!allocation) + { + m_render.pendingScreenshot = {}; + return false; + } + + bool mappedHere = false; + if (!allocation->getMappedPointer()) + { + const IDeviceMemoryAllocation::MemoryRange range = { 0u, texelBufferSize }; + if (!allocation->map(range, IDeviceMemoryAllocation::EMCAF_READ)) + { + m_render.pendingScreenshot = {}; + return false; + } + mappedHere = true; + } + + if (allocation->haveToMakeVisible()) + { + const ILogicalDevice::MappedMemoryRange mappedRange(allocation, 0u, texelBufferSize); + m_device->invalidateMappedMemoryRanges(1u, &mappedRange); + } - IAssetWriter::SAssetWriteParams params(cpuView.get()); - if (!m_assetMgr->writeAsset(path.string(), params)) + auto cpuImage = asset::ICPUImage::create(m_render.pendingScreenshot.imageParams); + auto cpuBuffer = asset::ICPUBuffer::create({ texelBufferSize }); + if (!cpuImage || !cpuBuffer) + { + if (mappedHere) + allocation->unmap(); + m_render.pendingScreenshot = {}; return false; + } - outImage = cpuView; + std::memcpy(cpuBuffer->getPointer(), allocation->getMappedPointer(), texelBufferSize); + + auto regions = core::make_refctd_dynamic_array>(1u); + auto& region = regions->front(); + region.imageSubresource.aspectMask = asset::IImage::EAF_COLOR_BIT; + region.imageSubresource.mipLevel = 0u; + region.imageSubresource.baseArrayLayer = 0u; + region.imageSubresource.layerCount = 1u; + region.bufferOffset = 0u; + region.bufferRowLength = m_render.pendingScreenshot.imageParams.extent.width; + region.bufferImageHeight = 0u; + region.imageOffset = { 0u, 0u, 0u }; + region.imageExtent = m_render.pendingScreenshot.imageParams.extent; + cpuImage->setBufferAndRegions(core::smart_refctd_ptr(cpuBuffer), regions); + + if (mappedHere) + allocation->unmap(); + + asset::ICPUImageView::SCreationParams viewParams = {}; + viewParams.image = std::move(cpuImage); + viewParams.format = m_render.pendingScreenshot.viewFormat; + viewParams.viewType = asset::ICPUImageView::ET_2D; + viewParams.subresourceRange = m_render.pendingScreenshot.subresourceRange; + + auto cpuView = asset::ICPUImageView::create(std::move(viewParams)); + if (!cpuView) + { + m_render.pendingScreenshot = {}; + return false; + } + + if (!m_render.pendingScreenshot.path.empty()) + { + const auto parentPath = m_render.pendingScreenshot.path.parent_path(); + if (!parentPath.empty()) + std::filesystem::create_directories(parentPath); + IAssetWriter::SAssetWriteParams params(cpuView.get()); + if (!m_assetMgr->writeAsset(m_render.pendingScreenshot.path.string(), params)) + { + m_render.pendingScreenshot = {}; + return false; + } + } + + outImage = std::move(cpuView); + ready = true; + m_render.pendingScreenshot = {}; return true; } @@ -155,15 +341,18 @@ void MeshLoadersApp::advanceCase() if (isRowViewActive()) return; - const uint32_t frameLimit = m_runtime.mode == RunMode::CI ? CiFramesBeforeCapture : NonCiFramesPerCase; - ++m_runtime.phaseFrameCounter; - if (m_runtime.phaseFrameCounter < frameLimit) - return; + const auto finalizePendingCapture = [this](core::smart_refctd_ptr& outImage, const char* const failureMessage) -> bool + { + bool ready = false; + if (!finalizeScreenshotCapture(outImage, ready)) + failExit("%s", failureMessage); + return ready; + }; - if (m_runtime.phase == Phase::RenderOriginal) + if (m_runtime.phase == Phase::CaptureOriginalPending) { - if (!captureScreenshot(m_output.loadedScreenshotPath, m_render.loadedScreenshot)) - failExit("Failed to capture loaded screenshot."); + if (!finalizePendingCapture(m_render.loadedScreenshot, "Failed to finalize loaded screenshot.")) + return; const bool canWriteCurrentAsset = m_output.saveGeom && static_cast(m_render.currentCpuAsset); if (m_output.saveGeom) @@ -200,10 +389,10 @@ void MeshLoadersApp::advanceCase() return; } - if (m_runtime.phase == Phase::RenderWritten) + if (m_runtime.phase == Phase::CaptureWrittenPending) { - if (!captureScreenshot(m_output.writtenScreenshotPath, m_render.writtenScreenshot)) - failExit("Failed to capture written screenshot."); + if (!finalizePendingCapture(m_render.writtenScreenshot, "Failed to finalize written screenshot.")) + return; uint64_t diffCodeUnitCount = 0u; uint32_t maxDiffCodeUnitValue = 0u; @@ -215,8 +404,27 @@ void MeshLoadersApp::advanceCase() m_logger->log("Image diff within tolerance for %s. CodeUnits: %llu MaxCodeUnitDiff: %u", ILogger::ELL_WARNING, m_caseName.c_str(), static_cast(diffCodeUnitCount), maxDiffCodeUnitValue); advanceToNextCase(); + return; } -} + const uint32_t frameLimit = m_runtime.mode == RunMode::CI ? CiFramesBeforeCapture : NonCiFramesPerCase; + const uint32_t captureRequestThreshold = (frameLimit > 1u) ? (frameLimit - 1u) : frameLimit; + ++m_runtime.phaseFrameCounter; + if (m_runtime.phaseFrameCounter < captureRequestThreshold) + return; + if (m_runtime.phase == Phase::RenderOriginal) + { + if (!requestScreenshotCapture(m_output.loadedScreenshotPath)) + failExit("Failed to request loaded screenshot capture."); + m_runtime.phase = Phase::CaptureOriginalPending; + return; + } + if (m_runtime.phase == Phase::RenderWritten) + { + if (!requestScreenshotCapture(m_output.writtenScreenshotPath)) + failExit("Failed to request written screenshot capture."); + m_runtime.phase = Phase::CaptureWrittenPending; + } +} From b38a4f75b684a046f26fce00c64dab54453f95cd Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Tue, 10 Mar 2026 22:13:48 +0100 Subject: [PATCH 26/34] Reduce mesh loaders test wall time --- 12_MeshLoaders/App.hpp | 48 ++++- 12_MeshLoaders/AppLifecycle.cpp | 9 + 12_MeshLoaders/AppLoad.cpp | 26 +-- 12_MeshLoaders/AppRuntime.cpp | 353 +++++++++++++++++++++++++++++++- 4 files changed, 415 insertions(+), 21 deletions(-) diff --git a/12_MeshLoaders/App.hpp b/12_MeshLoaders/App.hpp index 276bb8c25..e9f92586d 100644 --- a/12_MeshLoaders/App.hpp +++ b/12_MeshLoaders/App.hpp @@ -10,9 +10,12 @@ #include #include +#include #include +#include #include #include +#include #include #ifdef NBL_BUILD_DEBUG_DRAW @@ -62,6 +65,7 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built { RenderOriginal, CaptureOriginalPending, + WrittenAssetPending, RenderWritten, CaptureWrittenPending }; @@ -143,6 +147,39 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built } }; + struct WrittenAssetRequest + { + core::smart_refctd_ptr asset; + nbl::system::path path; + IAssetLoader::SAssetLoadParams loadParams = {}; + }; + + struct WrittenAssetResult + { + bool success = false; + std::string error; + nbl::system::path path; + std::string extension; + double openMs = 0.0; + double writeMs = 0.0; + double statMs = 0.0; + double totalWriteMs = 0.0; + double nonWriterMs = 0.0; + uintmax_t outputSize = 0u; + AssetLoadCallResult loadResult = {}; + }; + + struct BackgroundAssetWorker + { + std::mutex mutex; + std::condition_variable cv; + std::thread thread; + std::optional request; + std::optional result; + bool busy = false; + bool stop = false; + }; + struct RuntimeState { bool nonInteractiveTest = false; @@ -232,6 +269,7 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built void resetRowViewScene(); bool loadModel(const system::path& modelPath, bool updateCamera, bool storeCamera); + bool loadPreparedModel(const system::path& modelPath, AssetLoadCallResult&& loadResult, bool updateCamera, bool storeCamera); bool loadRowView(RowViewReloadMode mode); bool writeAssetRoot(smart_refctd_ptr asset, const std::string& savePath); @@ -257,8 +295,15 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built void logRowViewLoadTotal(double ms, size_t hits, size_t misses) const; bool validateWrittenAsset(const system::path& path); + static bool validateWrittenBundle(const asset::SAssetBundle& bundle); bool requestScreenshotCapture(const system::path& path); - bool finalizeScreenshotCapture(core::smart_refctd_ptr& outImage, bool& ready); + bool finalizeScreenshotCapture(core::smart_refctd_ptr& outImage, bool& ready, bool waitForCompletion=false); + bool startWrittenAssetWork(smart_refctd_ptr asset, const system::path& path); + bool finalizeWrittenAssetWork(WrittenAssetResult& result, bool& ready, bool waitForCompletion=false); + void logWrittenAssetWork(const WrittenAssetResult& result) const; + bool startBackgroundAssetWorker(); + void stopBackgroundAssetWorker(); + void backgroundAssetWorkerMain(); bool compareImages( const asset::ICPUImageView* a, const asset::ICPUImageView* b, @@ -279,6 +324,7 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built RuntimeState m_runtime; OutputState m_output; RowViewState m_rowView; + BackgroundAssetWorker m_backgroundAssetWorker; InputSystem::ChannelReader mouse; InputSystem::ChannelReader keyboard; diff --git a/12_MeshLoaders/AppLifecycle.cpp b/12_MeshLoaders/AppLifecycle.cpp index 50e102fe7..9710fd525 100644 --- a/12_MeshLoaders/AppLifecycle.cpp +++ b/12_MeshLoaders/AppLifecycle.cpp @@ -468,6 +468,8 @@ bool MeshLoadersApp::onAppInitialized(smart_refctd_ptr&& system) m_render.renderer = CSimpleDebugRenderer::create(m_assetMgr.get(), scRes->getRenderpass(), 0, {}); if (!m_render.renderer) return logFail("Failed to create renderer!"); + if (!startBackgroundAssetWorker()) + return logFail("Failed to start background asset worker."); #ifdef NBL_BUILD_DEBUG_DRAW { @@ -696,6 +698,7 @@ IQueue::SSubmitInfo::SSemaphoreInfo MeshLoadersApp::renderFrame(const std::chron bool MeshLoadersApp::onAppTerminated() { + stopBackgroundAssetWorker(); return device_base_t::onAppTerminated(); } @@ -945,6 +948,12 @@ bool MeshLoadersApp::startCase(const size_t index) if (!loadModel(testCase.path, true, true)) return false; + if (m_runtime.mode != RunMode::Interactive && m_output.saveGeom && m_render.currentCpuAsset) + { + if (!startWrittenAssetWork(m_render.currentCpuAsset, m_output.writtenPath)) + m_logger->log("Background written-asset preparation did not start for %s. Falling back to synchronous flow.", ILogger::ELL_WARNING, m_caseName.c_str()); + } + return true; } diff --git a/12_MeshLoaders/AppLoad.cpp b/12_MeshLoaders/AppLoad.cpp index f9bcddf7f..40be979cb 100644 --- a/12_MeshLoaders/AppLoad.cpp +++ b/12_MeshLoaders/AppLoad.cpp @@ -271,21 +271,23 @@ bool MeshLoadersApp::loadModel(const system::path& modelPath, bool updateCamera, failExit("Empty model path."); if (!std::filesystem::exists(modelPath)) failExit("Missing input: %s", modelPath.string().c_str()); + IAssetLoader::SAssetLoadParams params = makeLoadParams(); + AssetLoadCallResult loadResult = {}; + if (!loadAssetCallFromPath(modelPath, params, loadResult)) + failExit("Failed to open input file %s.", modelPath.string().c_str()); + return loadPreparedModel(modelPath, std::move(loadResult), updateCamera, storeCamera); +} + +bool MeshLoadersApp::loadPreparedModel(const system::path& modelPath, AssetLoadCallResult&& loadResult, bool updateCamera, bool storeCamera) +{ using clock_t = std::chrono::high_resolution_clock; - const auto loadOuterStart = clock_t::now(); m_modelPath = modelPath.string(); - // free up m_render.renderer->m_instances.clear(); m_render.renderer->clearGeometries({ .semaphore = m_render.semaphore.get(),.value = m_render.realFrameIx }); m_assetMgr->clearAllAssetCache(); - //! load the geometry - IAssetLoader::SAssetLoadParams params = makeLoadParams(); - AssetLoadCallResult loadResult = {}; - if (!loadAssetCallFromPath(modelPath, params, loadResult)) - failExit("Failed to open input file %s.", modelPath.string().c_str()); const auto loadMs = loadResult.getAssetMs; auto asset = std::move(loadResult.bundle); m_logger->log( @@ -294,9 +296,9 @@ bool MeshLoadersApp::loadModel(const system::path& modelPath, bool updateCamera, m_modelPath.c_str(), loadMs, static_cast(loadResult.inputSize)); - if (asset.getContents().empty()) - failExit("Failed to load asset %s.", m_modelPath.c_str()); - m_render.currentCpuAsset = (asset.getContents().size() == 1u) ? asset.getContents()[0] : nullptr; + if (asset.getContents().empty()) + failExit("Failed to load asset %s.", m_modelPath.c_str()); + m_render.currentCpuAsset = (asset.getContents().size() == 1u) ? asset.getContents()[0] : nullptr; PreparedGeometryBatch batch = {}; const auto extractStart = clock_t::now(); @@ -307,8 +309,8 @@ bool MeshLoadersApp::loadModel(const system::path& modelPath, bool updateCamera, if (batch.geometries.empty()) failExit("No geometry found in asset %s.", m_modelPath.c_str()); - const auto outerMs = toMs(clock_t::now() - loadOuterStart); - const auto nonLoaderMs = std::max(0.0, outerMs - loadMs); + const auto outerMs = loadMs + extractMs; + const auto nonLoaderMs = extractMs; m_logger->log( "Asset load outer perf: path=%s getAsset=%.3f ms extract=%.3f ms total=%.3f ms non_loader=%.3f ms", ILogger::ELL_INFO, diff --git a/12_MeshLoaders/AppRuntime.cpp b/12_MeshLoaders/AppRuntime.cpp index 1a430df8e..f7422d889 100644 --- a/12_MeshLoaders/AppRuntime.cpp +++ b/12_MeshLoaders/AppRuntime.cpp @@ -94,15 +94,217 @@ bool MeshLoadersApp::validateWrittenAsset(const system::path& path) IAssetLoader::SAssetLoadParams params = makeLoadParams(); auto asset = m_assetMgr->getAsset(path.string(), params); - if (asset.getContents().empty()) - return false; + return validateWrittenBundle(asset); +} +bool MeshLoadersApp::validateWrittenBundle(const asset::SAssetBundle& bundle) +{ core::vector geometries; - if (!meshloaders::collectBundleGeometryItems(asset, geometries, false)) + if (!meshloaders::collectBundleGeometryItems(bundle, geometries, false)) return false; return !geometries.empty(); } +bool MeshLoadersApp::startBackgroundAssetWorker() +{ + auto& worker = m_backgroundAssetWorker; + if (worker.thread.joinable()) + return true; + + worker.stop = false; + worker.thread = std::thread(&MeshLoadersApp::backgroundAssetWorkerMain, this); + return true; +} + +void MeshLoadersApp::stopBackgroundAssetWorker() +{ + auto& worker = m_backgroundAssetWorker; + { + std::lock_guard lock(worker.mutex); + worker.stop = true; + } + worker.cv.notify_all(); + if (worker.thread.joinable()) + worker.thread.join(); + + std::lock_guard lock(worker.mutex); + worker.request.reset(); + worker.result.reset(); + worker.busy = false; + worker.stop = false; +} + +void MeshLoadersApp::backgroundAssetWorkerMain() +{ + auto workerSystem = core::smart_refctd_ptr(m_system); + auto workerAssetMgr = workerSystem ? core::make_smart_refctd_ptr(core::smart_refctd_ptr(workerSystem)) : nullptr; + + for (;;) + { + WrittenAssetRequest request = {}; + { + std::unique_lock lock(m_backgroundAssetWorker.mutex); + m_backgroundAssetWorker.cv.wait(lock, [this] { + return m_backgroundAssetWorker.stop || m_backgroundAssetWorker.request.has_value(); + }); + if (m_backgroundAssetWorker.stop && !m_backgroundAssetWorker.request.has_value()) + break; + request = std::move(*m_backgroundAssetWorker.request); + m_backgroundAssetWorker.request.reset(); + } + + WrittenAssetResult result = {}; + result.path = request.path; + result.extension = normalizeExtension(request.path); + + if (!workerSystem || !workerAssetMgr) + { + result.error = "Background asset worker is unavailable."; + } + else if (!request.asset) + { + result.error = "Background asset worker received an empty asset."; + } + else + { + using clock_t = std::chrono::high_resolution_clock; + const auto writeOuterStart = clock_t::now(); + auto* const assetPtr = const_cast(request.asset.get()); + auto flags = asset::EWF_MESH_IS_RIGHT_HANDED; + if (result.extension != ".obj") + flags = static_cast(flags | asset::EWF_BINARY); + IAssetWriter::SAssetWriteParams writeParams{ assetPtr, flags }; + writeParams.logger = request.loadParams.logger; + + const auto openStart = clock_t::now(); + system::ISystem::future_t> writeFileFuture; + workerSystem->createFile(writeFileFuture, request.path, system::IFile::ECF_WRITE); + core::smart_refctd_ptr writeFile; + writeFileFuture.acquire().move_into(writeFile); + result.openMs = toMs(clock_t::now() - openStart); + if (!writeFile) + { + result.error = "Background asset worker failed to open the output file."; + } + else + { + const auto writeStart = clock_t::now(); + if (!workerAssetMgr->writeAsset(writeFile.get(), writeParams)) + { + result.error = "Background asset worker failed to write the asset."; + } + result.writeMs = toMs(clock_t::now() - writeStart); + writeFile = nullptr; + } + + if (result.error.empty()) + { + const auto statStart = clock_t::now(); + if (std::filesystem::exists(request.path)) + result.outputSize = std::filesystem::file_size(request.path); + result.statMs = toMs(clock_t::now() - statStart); + result.totalWriteMs = toMs(clock_t::now() - writeOuterStart); + result.nonWriterMs = std::max(0.0, result.totalWriteMs - result.writeMs); + + workerAssetMgr->clearAllAssetCache(); + if (std::filesystem::exists(request.path)) + result.loadResult.inputSize = std::filesystem::file_size(request.path); + else + result.loadResult.inputSize = 0u; + + const auto loadStart = clock_t::now(); + result.loadResult.bundle = workerAssetMgr->getAsset(request.path.string(), request.loadParams); + result.loadResult.getAssetMs = toMs(clock_t::now() - loadStart); + if (result.loadResult.bundle.getContents().empty()) + result.error = "Background asset worker failed to load the written asset."; + } + } + + result.success = result.error.empty(); + { + std::lock_guard lock(m_backgroundAssetWorker.mutex); + m_backgroundAssetWorker.result = std::move(result); + m_backgroundAssetWorker.busy = false; + } + m_backgroundAssetWorker.cv.notify_all(); + } +} + +bool MeshLoadersApp::startWrittenAssetWork(smart_refctd_ptr asset, const system::path& path) +{ + if (!asset || path.empty()) + return false; + if (!startBackgroundAssetWorker()) + return false; + + std::unique_lock lock(m_backgroundAssetWorker.mutex); + if (m_backgroundAssetWorker.busy || m_backgroundAssetWorker.request.has_value() || m_backgroundAssetWorker.result.has_value()) + return false; + + m_backgroundAssetWorker.request = WrittenAssetRequest{ + .asset = std::move(asset), + .path = path, + .loadParams = makeLoadParams() + }; + m_backgroundAssetWorker.busy = true; + lock.unlock(); + m_backgroundAssetWorker.cv.notify_one(); + return true; +} + +bool MeshLoadersApp::finalizeWrittenAssetWork(WrittenAssetResult& result, bool& ready, bool waitForCompletion) +{ + ready = false; + if (!m_backgroundAssetWorker.thread.joinable()) + return false; + + std::unique_lock lock(m_backgroundAssetWorker.mutex); + if (waitForCompletion) + { + m_backgroundAssetWorker.cv.wait(lock, [this] { + return !m_backgroundAssetWorker.busy && !m_backgroundAssetWorker.request.has_value(); + }); + } + if (m_backgroundAssetWorker.result.has_value()) + { + result = std::move(*m_backgroundAssetWorker.result); + m_backgroundAssetWorker.result.reset(); + ready = true; + return true; + } + return m_backgroundAssetWorker.busy || m_backgroundAssetWorker.request.has_value(); +} + +void MeshLoadersApp::logWrittenAssetWork(const WrittenAssetResult& result) const +{ + m_logger->log( + "Asset write call perf: path=%s ext=%s time=%.3f ms size=%llu", + ILogger::ELL_INFO, + result.path.string().c_str(), + result.extension.c_str(), + result.writeMs, + static_cast(result.outputSize)); + m_logger->log( + "Asset write outer perf: path=%s ext=%s open=%.3f ms writeAsset=%.3f ms stat=%.3f ms total=%.3f ms non_writer=%.3f ms size=%llu", + ILogger::ELL_INFO, + result.path.string().c_str(), + result.extension.c_str(), + result.openMs, + result.writeMs, + result.statMs, + result.totalWriteMs, + result.nonWriterMs, + static_cast(result.outputSize)); + m_logger->log( + "Writer perf: path=%s ext=%s time=%.3f ms size=%llu", + ILogger::ELL_INFO, + result.path.string().c_str(), + result.extension.c_str(), + result.writeMs, + static_cast(result.outputSize)); + m_logger->log("Mesh successfully saved!", ILogger::ELL_INFO); +} + bool MeshLoadersApp::requestScreenshotCapture(const system::path& path) { if (!m_device || !m_surface || !m_assetMgr) @@ -230,12 +432,25 @@ bool MeshLoadersApp::requestScreenshotCapture(const system::path& path) return true; } -bool MeshLoadersApp::finalizeScreenshotCapture(core::smart_refctd_ptr& outImage, bool& ready) +bool MeshLoadersApp::finalizeScreenshotCapture(core::smart_refctd_ptr& outImage, bool& ready, bool waitForCompletion) { ready = false; if (!m_render.pendingScreenshot.active()) return false; + if (waitForCompletion) + { + const ISemaphore::SWaitInfo waitInfo = { + .semaphore = m_render.pendingScreenshot.completionSemaphore.get(), + .value = m_render.pendingScreenshot.completionValue + }; + if (m_device->blockForSemaphores({ &waitInfo, 1u }) != ISemaphore::WAIT_RESULT::SUCCESS) + { + m_render.pendingScreenshot = {}; + return false; + } + } + if (m_render.pendingScreenshot.completionSemaphore->getCounterValue() < m_render.pendingScreenshot.completionValue) return true; @@ -341,14 +556,89 @@ void MeshLoadersApp::advanceCase() if (isRowViewActive()) return; - const auto finalizePendingCapture = [this](core::smart_refctd_ptr& outImage, const char* const failureMessage) -> bool + const auto finalizePendingCapture = [this](core::smart_refctd_ptr& outImage, const char* const failureMessage, const bool waitForCompletion = false) -> bool { bool ready = false; - if (!finalizeScreenshotCapture(outImage, ready)) + if (!finalizeScreenshotCapture(outImage, ready, waitForCompletion)) failExit("%s", failureMessage); return ready; }; + const auto handleWrittenAssetReady = [this](WrittenAssetResult&& result) -> void + { + if (!result.success) + failExit("%s", result.error.c_str()); + logWrittenAssetWork(result); + + if (m_runtime.mode == RunMode::CI) + { + if (!loadPreparedModel(m_output.writtenPath, std::move(result.loadResult), false, false)) + failExit("Failed to load written asset %s.", m_output.writtenPath.string().c_str()); + if (!m_render.currentCpuGeom) + failExit("Written geometry missing."); + m_runtime.phase = Phase::RenderWritten; + m_runtime.phaseFrameCounter = 0u; + return; + } + + if (!validateWrittenBundle(result.loadResult.bundle)) + failExit("Failed to load written asset %s.", m_output.writtenPath.string().c_str()); + + advanceToNextCase(); + }; + + if (m_runtime.mode == RunMode::CI && m_runtime.phase == Phase::RenderOriginal) + { + ++m_runtime.phaseFrameCounter; + if (m_runtime.phaseFrameCounter == 0u) + return; + + if (!requestScreenshotCapture(m_output.loadedScreenshotPath)) + failExit("Failed to request loaded screenshot capture."); + if (!finalizePendingCapture(m_render.loadedScreenshot, "Failed to finalize loaded screenshot.", true)) + failExit("Loaded screenshot capture did not complete."); + + const bool canWriteCurrentAsset = m_output.saveGeom && static_cast(m_render.currentCpuAsset); + if (m_output.saveGeom && !canWriteCurrentAsset) + m_logger->log("Skipping write/reload for %s because the loaded case expands to multiple root geometries.", ILogger::ELL_INFO, m_caseName.c_str()); + if (!canWriteCurrentAsset) + { + advanceToNextCase(); + return; + } + + WrittenAssetResult result = {}; + bool ready = false; + if (!finalizeWrittenAssetWork(result, ready, true) || !ready) + failExit("Written asset preparation did not complete."); + handleWrittenAssetReady(std::move(result)); + return; + } + + if (m_runtime.mode == RunMode::CI && m_runtime.phase == Phase::RenderWritten) + { + ++m_runtime.phaseFrameCounter; + if (m_runtime.phaseFrameCounter == 0u) + return; + + if (!requestScreenshotCapture(m_output.writtenScreenshotPath)) + failExit("Failed to request written screenshot capture."); + if (!finalizePendingCapture(m_render.writtenScreenshot, "Failed to finalize written screenshot.", true)) + failExit("Written screenshot capture did not complete."); + + uint64_t diffCodeUnitCount = 0u; + uint32_t maxDiffCodeUnitValue = 0u; + if (!compareImages(m_render.loadedScreenshot.get(), m_render.writtenScreenshot.get(), diffCodeUnitCount, maxDiffCodeUnitValue)) + failExit("Image compare failed for %s.", m_caseName.c_str()); + if (diffCodeUnitCount > MaxImageDiffCodeUnits || maxDiffCodeUnitValue > MaxImageDiffCodeUnitValue) + failExit("Image diff detected for %s. CodeUnits: %llu MaxCodeUnitDiff: %u", m_caseName.c_str(), static_cast(diffCodeUnitCount), maxDiffCodeUnitValue); + if (diffCodeUnitCount != 0u) + m_logger->log("Image diff within tolerance for %s. CodeUnits: %llu MaxCodeUnitDiff: %u", ILogger::ELL_WARNING, m_caseName.c_str(), static_cast(diffCodeUnitCount), maxDiffCodeUnitValue); + + advanceToNextCase(); + return; + } + if (m_runtime.phase == Phase::CaptureOriginalPending) { if (!finalizePendingCapture(m_render.loadedScreenshot, "Failed to finalize loaded screenshot.")) @@ -359,8 +649,43 @@ void MeshLoadersApp::advanceCase() { if (!canWriteCurrentAsset) m_logger->log("Skipping write/reload for %s because the loaded case expands to multiple root geometries.", ILogger::ELL_INFO, m_caseName.c_str()); - else if (!writeAssetRoot(m_render.currentCpuAsset, m_output.writtenPath.string())) - failExit("Geometry write failed."); + else + { + WrittenAssetResult result = {}; + bool ready = false; + const bool workerStateValid = finalizeWrittenAssetWork(result, ready); + if (!workerStateValid) + { + if (!writeAssetRoot(m_render.currentCpuAsset, m_output.writtenPath.string())) + failExit("Geometry write failed."); + + if (m_runtime.mode == RunMode::CI) + { + if (!loadModel(m_output.writtenPath, false, false)) + failExit("Failed to load written asset %s.", m_output.writtenPath.string().c_str()); + if (!m_render.currentCpuGeom) + failExit("Written geometry missing."); + m_runtime.phase = Phase::RenderWritten; + m_runtime.phaseFrameCounter = 0u; + return; + } + + if (!validateWrittenAsset(m_output.writtenPath)) + failExit("Failed to load written asset %s.", m_output.writtenPath.string().c_str()); + + advanceToNextCase(); + return; + } + + if (!ready) + { + m_runtime.phase = Phase::WrittenAssetPending; + return; + } + + handleWrittenAssetReady(std::move(result)); + return; + } } if (m_runtime.mode == RunMode::CI) @@ -389,6 +714,18 @@ void MeshLoadersApp::advanceCase() return; } + if (m_runtime.phase == Phase::WrittenAssetPending) + { + WrittenAssetResult result = {}; + bool ready = false; + if (!finalizeWrittenAssetWork(result, ready)) + failExit("Background written asset work failed unexpectedly."); + if (!ready) + return; + handleWrittenAssetReady(std::move(result)); + return; + } + if (m_runtime.phase == Phase::CaptureWrittenPending) { if (!finalizePendingCapture(m_render.writtenScreenshot, "Failed to finalize written screenshot.")) From ec33704c3c3a9771373ef09be0a61f1b655d40d6 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Wed, 11 Mar 2026 08:58:52 +0100 Subject: [PATCH 27/34] Accelerate mesh loaders roundtrip flow --- 12_MeshLoaders/App.hpp | 32 ++++ 12_MeshLoaders/AppLifecycle.cpp | 46 ++++- 12_MeshLoaders/AppLoad.cpp | 4 +- 12_MeshLoaders/AppRuntime.cpp | 300 ++++++++++++++++++++++++++++---- 4 files changed, 341 insertions(+), 41 deletions(-) diff --git a/12_MeshLoaders/App.hpp b/12_MeshLoaders/App.hpp index e9f92586d..01493f7dc 100644 --- a/12_MeshLoaders/App.hpp +++ b/12_MeshLoaders/App.hpp @@ -152,6 +152,9 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built core::smart_refctd_ptr asset; nbl::system::path path; IAssetLoader::SAssetLoadParams loadParams = {}; + bool useMemoryTransport = false; + bool allowDiskFallback = false; + bool persistDiskArtifact = false; }; struct WrittenAssetResult @@ -180,6 +183,28 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built bool stop = false; }; + struct PreparedAssetLoad + { + size_t caseIndex = ~size_t(0u); + bool success = false; + std::string error; + nbl::system::path path; + AssetLoadCallResult loadResult = {}; + }; + + struct BackgroundLoadWorker + { + std::mutex mutex; + std::condition_variable cv; + std::thread thread; + std::optional requestCaseIndex; + nbl::system::path requestPath; + IAssetLoader::SAssetLoadParams requestParams = {}; + std::optional result; + bool busy = false; + bool stop = false; + }; + struct RuntimeState { bool nonInteractiveTest = false; @@ -257,6 +282,7 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built bool isRowViewActive() const; static std::string normalizeExtension(const system::path& path); + asset::writer_flags_t getWriterFlagsForPath(const IAsset* asset, const system::path& path) const; bool isWriteExtensionSupported(const std::string& ext) const; system::path resolveSavePath(const system::path& modelPath) const; @@ -304,6 +330,11 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built bool startBackgroundAssetWorker(); void stopBackgroundAssetWorker(); void backgroundAssetWorkerMain(); + bool startBackgroundLoadWorker(); + void stopBackgroundLoadWorker(); + void backgroundLoadWorkerMain(); + bool startPreparedAssetLoad(size_t caseIndex, const system::path& path); + bool finalizePreparedAssetLoad(PreparedAssetLoad& result, bool& ready, bool waitForCompletion=false); bool compareImages( const asset::ICPUImageView* a, const asset::ICPUImageView* b, @@ -325,6 +356,7 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built OutputState m_output; RowViewState m_rowView; BackgroundAssetWorker m_backgroundAssetWorker; + BackgroundLoadWorker m_backgroundLoadWorker; InputSystem::ChannelReader mouse; InputSystem::ChannelReader keyboard; diff --git a/12_MeshLoaders/AppLifecycle.cpp b/12_MeshLoaders/AppLifecycle.cpp index 9710fd525..dfddde5c4 100644 --- a/12_MeshLoaders/AppLifecycle.cpp +++ b/12_MeshLoaders/AppLifecycle.cpp @@ -470,6 +470,8 @@ bool MeshLoadersApp::onAppInitialized(smart_refctd_ptr&& system) return logFail("Failed to create renderer!"); if (!startBackgroundAssetWorker()) return logFail("Failed to start background asset worker."); + if (!startBackgroundLoadWorker()) + return logFail("Failed to start background load worker."); #ifdef NBL_BUILD_DEBUG_DRAW { @@ -698,6 +700,7 @@ IQueue::SSubmitInfo::SSemaphoreInfo MeshLoadersApp::renderFrame(const std::chron bool MeshLoadersApp::onAppTerminated() { + stopBackgroundLoadWorker(); stopBackgroundAssetWorker(); return device_base_t::onAppTerminated(); } @@ -888,6 +891,24 @@ std::string MeshLoadersApp::normalizeExtension(const system::path& path) return ext; } +asset::writer_flags_t MeshLoadersApp::getWriterFlagsForPath(const IAsset* const asset, const system::path& path) const +{ + if (!asset) + return asset::EWF_NONE; + + const auto extension = system::extension_wo_dot(path); + auto flags = asset::writer_flags_t(asset::EWF_NONE); + if (const auto writerInfo = m_assetMgr->getAssetWriterFlagInfo(asset->getAssetType(), extension); writerInfo.has_value()) + { + flags = writerInfo->forced; + const auto preferred = asset::writer_flags_t(asset::EWF_MESH_IS_RIGHT_HANDED | asset::EWF_BINARY); + flags |= preferred & writerInfo->supported; + return flags; + } + + return asset::writer_flags_t(asset::EWF_MESH_IS_RIGHT_HANDED); +} + bool MeshLoadersApp::isWriteExtensionSupported(const std::string& ext) const { if (ext == ".ply" || ext == ".stl") @@ -945,13 +966,36 @@ bool MeshLoadersApp::startCase(const size_t index) m_output.loadedScreenshotPath = artifacts.loadedScreenshotPath; m_output.writtenScreenshotPath = artifacts.writtenScreenshotPath; - if (!loadModel(testCase.path, true, true)) + bool loaded = false; + if (m_runtime.mode == RunMode::CI) + { + PreparedAssetLoad preparedLoad = {}; + bool preparedReady = false; + const bool loadWorkerStateValid = finalizePreparedAssetLoad(preparedLoad, preparedReady, true); + if (loadWorkerStateValid && preparedReady && preparedLoad.success && preparedLoad.caseIndex == index && preparedLoad.path == testCase.path) + loaded = loadPreparedModel(testCase.path, std::move(preparedLoad.loadResult), true, true); + else + loaded = loadModel(testCase.path, true, true); + } + else + loaded = loadModel(testCase.path, true, true); + if (!loaded) return false; if (m_runtime.mode != RunMode::Interactive && m_output.saveGeom && m_render.currentCpuAsset) { if (!startWrittenAssetWork(m_render.currentCpuAsset, m_output.writtenPath)) + { + if (m_runtime.mode == RunMode::CI) + return logFail("Background written-asset preparation did not start for %s.", m_caseName.c_str()); m_logger->log("Background written-asset preparation did not start for %s. Falling back to synchronous flow.", ILogger::ELL_WARNING, m_caseName.c_str()); + } + } + if (m_runtime.mode == RunMode::CI) + { + const auto nextIndex = index + 1u; + if (nextIndex < m_runtime.cases.size()) + startPreparedAssetLoad(nextIndex, m_runtime.cases[nextIndex].path); } return true; diff --git a/12_MeshLoaders/AppLoad.cpp b/12_MeshLoaders/AppLoad.cpp index 40be979cb..ebf6dad86 100644 --- a/12_MeshLoaders/AppLoad.cpp +++ b/12_MeshLoaders/AppLoad.cpp @@ -767,9 +767,7 @@ bool MeshLoadersApp::writeAssetRoot(smart_refctd_ptr asset, const IAsset* assetPtr = const_cast(asset.get()); const auto ext = normalizeExtension(system::path(savePath)); - auto flags = asset::EWF_MESH_IS_RIGHT_HANDED; - if (ext != ".obj") - flags = static_cast(flags | asset::EWF_BINARY); + const auto flags = getWriterFlagsForPath(asset.get(), system::path(savePath)); IAssetWriter::SAssetWriteParams params{ assetPtr, flags }; params.logger = getAssetLoadLogger(); m_logger->log("Saving mesh to %s", ILogger::ELL_INFO, savePath.c_str()); diff --git a/12_MeshLoaders/AppRuntime.cpp b/12_MeshLoaders/AppRuntime.cpp index f7422d889..4394487db 100644 --- a/12_MeshLoaders/AppRuntime.cpp +++ b/12_MeshLoaders/AppRuntime.cpp @@ -7,9 +7,34 @@ #include "nbl/examples/common/ImageComparison.h" #include "nbl/builtin/hlsl/math/linalg/fast_affine.hlsl" +#include "nbl/system/CGrowableMemoryFile.h" #include +namespace +{ + +bool persistMemoryFileToDisk(system::ISystem* const system, const system::path& path, const system::CGrowableMemoryFile* const file) +{ + if (!system || !file) + return false; + + system::ISystem::future_t> writeFileFuture; + system->createFile(writeFileFuture, path, system::IFile::ECF_WRITE); + core::smart_refctd_ptr writeFile; + writeFileFuture.acquire().move_into(writeFile); + if (!writeFile) + return false; + + const auto* const data = file->data(); + const auto size = file->getSize(); + system::IFile::success_t success; + writeFile->write(success, data, 0ull, size); + return static_cast(success); +} + +} + std::string MeshLoadersApp::makeUniqueCaseName(const system::path& path) { auto base = path.stem().string(); @@ -134,6 +159,36 @@ void MeshLoadersApp::stopBackgroundAssetWorker() worker.stop = false; } +bool MeshLoadersApp::startBackgroundLoadWorker() +{ + auto& worker = m_backgroundLoadWorker; + if (worker.thread.joinable()) + return true; + + worker.stop = false; + worker.thread = std::thread(&MeshLoadersApp::backgroundLoadWorkerMain, this); + return true; +} + +void MeshLoadersApp::stopBackgroundLoadWorker() +{ + auto& worker = m_backgroundLoadWorker; + { + std::lock_guard lock(worker.mutex); + worker.stop = true; + } + worker.cv.notify_all(); + if (worker.thread.joinable()) + worker.thread.join(); + + std::lock_guard lock(worker.mutex); + worker.requestCaseIndex.reset(); + worker.requestPath.clear(); + worker.result.reset(); + worker.busy = false; + worker.stop = false; +} + void MeshLoadersApp::backgroundAssetWorkerMain() { auto workerSystem = core::smart_refctd_ptr(m_system); @@ -170,53 +225,120 @@ void MeshLoadersApp::backgroundAssetWorkerMain() using clock_t = std::chrono::high_resolution_clock; const auto writeOuterStart = clock_t::now(); auto* const assetPtr = const_cast(request.asset.get()); - auto flags = asset::EWF_MESH_IS_RIGHT_HANDED; - if (result.extension != ".obj") - flags = static_cast(flags | asset::EWF_BINARY); + const auto flags = getWriterFlagsForPath(request.asset.get(), request.path); IAssetWriter::SAssetWriteParams writeParams{ assetPtr, flags }; writeParams.logger = request.loadParams.logger; - const auto openStart = clock_t::now(); - system::ISystem::future_t> writeFileFuture; - workerSystem->createFile(writeFileFuture, request.path, system::IFile::ECF_WRITE); - core::smart_refctd_ptr writeFile; - writeFileFuture.acquire().move_into(writeFile); - result.openMs = toMs(clock_t::now() - openStart); - if (!writeFile) + bool useDiskTransport = !request.useMemoryTransport; + if (request.useMemoryTransport) { - result.error = "Background asset worker failed to open the output file."; - } - else - { - const auto writeStart = clock_t::now(); - if (!workerAssetMgr->writeAsset(writeFile.get(), writeParams)) + auto memoryFile = core::make_smart_refctd_ptr(system::path(request.path)); + bool memoryTransportSucceeded = false; + + if (memoryFile) + { + const auto writeStart = clock_t::now(); + if (!workerAssetMgr->writeAsset(memoryFile.get(), writeParams)) + { + if (request.allowDiskFallback) + useDiskTransport = true; + else + result.error = "Background asset worker failed to write the asset to the in-memory transport."; + } + else + { + result.writeMs = toMs(clock_t::now() - writeStart); + result.outputSize = memoryFile->getSize(); + result.totalWriteMs = toMs(clock_t::now() - writeOuterStart); + result.nonWriterMs = std::max(0.0, result.totalWriteMs - result.writeMs); + + workerAssetMgr->clearAllAssetCache(); + result.loadResult.inputSize = memoryFile->getSize(); + const auto loadStart = clock_t::now(); + result.loadResult.bundle = workerAssetMgr->getAsset(memoryFile.get(), request.path.string(), request.loadParams); + result.loadResult.getAssetMs = toMs(clock_t::now() - loadStart); + if (result.loadResult.bundle.getContents().empty()) + { + if (request.allowDiskFallback) + useDiskTransport = true; + else + result.error = "Background asset worker failed to load the in-memory written asset."; + } + else + memoryTransportSucceeded = true; + } + + if (memoryTransportSucceeded && request.persistDiskArtifact) + { + if (!persistMemoryFileToDisk(workerSystem.get(), request.path, memoryFile.get())) + { + if (request.allowDiskFallback) + useDiskTransport = true; + else + result.error = "Background asset worker failed to persist the in-memory written asset."; + if (useDiskTransport) + { + result.loadResult = {}; + result.outputSize = 0u; + result.totalWriteMs = 0.0; + result.nonWriterMs = 0.0; + } + } + } + } + else { - result.error = "Background asset worker failed to write the asset."; + if (request.allowDiskFallback) + useDiskTransport = true; + else + result.error = "Background asset worker could not create the in-memory transport."; } - result.writeMs = toMs(clock_t::now() - writeStart); - writeFile = nullptr; } - if (result.error.empty()) + if (useDiskTransport && result.error.empty()) { - const auto statStart = clock_t::now(); - if (std::filesystem::exists(request.path)) - result.outputSize = std::filesystem::file_size(request.path); - result.statMs = toMs(clock_t::now() - statStart); - result.totalWriteMs = toMs(clock_t::now() - writeOuterStart); - result.nonWriterMs = std::max(0.0, result.totalWriteMs - result.writeMs); - - workerAssetMgr->clearAllAssetCache(); - if (std::filesystem::exists(request.path)) - result.loadResult.inputSize = std::filesystem::file_size(request.path); + const auto openStart = clock_t::now(); + system::ISystem::future_t> writeFileFuture; + workerSystem->createFile(writeFileFuture, request.path, system::IFile::ECF_WRITE); + core::smart_refctd_ptr writeFile; + writeFileFuture.acquire().move_into(writeFile); + result.openMs = toMs(clock_t::now() - openStart); + if (!writeFile) + { + result.error = "Background asset worker failed to open the output file."; + } else - result.loadResult.inputSize = 0u; + { + const auto writeStart = clock_t::now(); + if (!workerAssetMgr->writeAsset(writeFile.get(), writeParams)) + { + result.error = "Background asset worker failed to write the asset."; + } + result.writeMs = toMs(clock_t::now() - writeStart); + writeFile = nullptr; + } - const auto loadStart = clock_t::now(); - result.loadResult.bundle = workerAssetMgr->getAsset(request.path.string(), request.loadParams); - result.loadResult.getAssetMs = toMs(clock_t::now() - loadStart); - if (result.loadResult.bundle.getContents().empty()) - result.error = "Background asset worker failed to load the written asset."; + if (result.error.empty()) + { + const auto statStart = clock_t::now(); + if (std::filesystem::exists(request.path)) + result.outputSize = std::filesystem::file_size(request.path); + result.statMs = toMs(clock_t::now() - statStart); + result.totalWriteMs = toMs(clock_t::now() - writeOuterStart); + result.nonWriterMs = std::max(0.0, result.totalWriteMs - result.writeMs); + + workerAssetMgr->clearAllAssetCache(); + if (std::filesystem::exists(request.path)) + result.loadResult.inputSize = std::filesystem::file_size(request.path); + else + result.loadResult.inputSize = 0u; + + const auto loadStart = clock_t::now(); + result.loadResult.bundle = workerAssetMgr->getAsset(request.path.string(), request.loadParams); + result.loadResult.getAssetMs = toMs(clock_t::now() - loadStart); + if (result.loadResult.bundle.getContents().empty()) + result.error = "Background asset worker failed to load the written asset."; + } } } @@ -230,6 +352,61 @@ void MeshLoadersApp::backgroundAssetWorkerMain() } } +void MeshLoadersApp::backgroundLoadWorkerMain() +{ + auto workerSystem = core::smart_refctd_ptr(m_system); + auto workerAssetMgr = workerSystem ? core::make_smart_refctd_ptr(core::smart_refctd_ptr(workerSystem)) : nullptr; + + for (;;) + { + std::optional caseIndex; + system::path requestPath; + IAssetLoader::SAssetLoadParams requestParams = {}; + { + std::unique_lock lock(m_backgroundLoadWorker.mutex); + m_backgroundLoadWorker.cv.wait(lock, [this] { + return m_backgroundLoadWorker.stop || m_backgroundLoadWorker.requestCaseIndex.has_value(); + }); + if (m_backgroundLoadWorker.stop && !m_backgroundLoadWorker.requestCaseIndex.has_value()) + break; + caseIndex = m_backgroundLoadWorker.requestCaseIndex; + requestPath = m_backgroundLoadWorker.requestPath; + requestParams = m_backgroundLoadWorker.requestParams; + m_backgroundLoadWorker.requestCaseIndex.reset(); + m_backgroundLoadWorker.requestPath.clear(); + } + + PreparedAssetLoad result = {}; + result.caseIndex = *caseIndex; + result.path = requestPath; + if (!workerAssetMgr) + { + result.error = "Background load worker is unavailable."; + } + else if (!std::filesystem::exists(requestPath)) + { + result.error = "Background load worker did not find the requested input."; + } + else + { + workerAssetMgr->clearAllAssetCache(); + result.loadResult.inputSize = std::filesystem::file_size(requestPath); + const auto loadStart = std::chrono::high_resolution_clock::now(); + result.loadResult.bundle = workerAssetMgr->getAsset(requestPath.string(), requestParams); + result.loadResult.getAssetMs = toMs(std::chrono::high_resolution_clock::now() - loadStart); + if (result.loadResult.bundle.getContents().empty()) + result.error = "Background load worker failed to load the requested asset."; + } + result.success = result.error.empty(); + { + std::lock_guard lock(m_backgroundLoadWorker.mutex); + m_backgroundLoadWorker.result = std::move(result); + m_backgroundLoadWorker.busy = false; + } + m_backgroundLoadWorker.cv.notify_all(); + } +} + bool MeshLoadersApp::startWrittenAssetWork(smart_refctd_ptr asset, const system::path& path) { if (!asset || path.empty()) @@ -244,7 +421,10 @@ bool MeshLoadersApp::startWrittenAssetWork(smart_refctd_ptr asset, m_backgroundAssetWorker.request = WrittenAssetRequest{ .asset = std::move(asset), .path = path, - .loadParams = makeLoadParams() + .loadParams = makeLoadParams(), + .useMemoryTransport = true, + .allowDiskFallback = (m_runtime.mode != RunMode::CI), + .persistDiskArtifact = (m_runtime.mode != RunMode::CI) }; m_backgroundAssetWorker.busy = true; lock.unlock(); @@ -275,6 +455,50 @@ bool MeshLoadersApp::finalizeWrittenAssetWork(WrittenAssetResult& result, bool& return m_backgroundAssetWorker.busy || m_backgroundAssetWorker.request.has_value(); } +bool MeshLoadersApp::startPreparedAssetLoad(const size_t caseIndex, const system::path& path) +{ + if (path.empty()) + return false; + if (!startBackgroundLoadWorker()) + return false; + + std::unique_lock lock(m_backgroundLoadWorker.mutex); + if (m_backgroundLoadWorker.busy || m_backgroundLoadWorker.requestCaseIndex.has_value()) + return false; + + m_backgroundLoadWorker.result.reset(); + m_backgroundLoadWorker.requestCaseIndex = caseIndex; + m_backgroundLoadWorker.requestPath = path; + m_backgroundLoadWorker.requestParams = makeLoadParams(); + m_backgroundLoadWorker.busy = true; + lock.unlock(); + m_backgroundLoadWorker.cv.notify_one(); + return true; +} + +bool MeshLoadersApp::finalizePreparedAssetLoad(PreparedAssetLoad& result, bool& ready, bool waitForCompletion) +{ + ready = false; + if (!m_backgroundLoadWorker.thread.joinable()) + return false; + + std::unique_lock lock(m_backgroundLoadWorker.mutex); + if (waitForCompletion) + { + m_backgroundLoadWorker.cv.wait(lock, [this] { + return !m_backgroundLoadWorker.busy && !m_backgroundLoadWorker.requestCaseIndex.has_value(); + }); + } + if (m_backgroundLoadWorker.result.has_value()) + { + result = std::move(*m_backgroundLoadWorker.result); + m_backgroundLoadWorker.result.reset(); + ready = true; + return true; + } + return m_backgroundLoadWorker.busy || m_backgroundLoadWorker.requestCaseIndex.has_value(); +} + void MeshLoadersApp::logWrittenAssetWork(const WrittenAssetResult& result) const { m_logger->log( @@ -656,6 +880,8 @@ void MeshLoadersApp::advanceCase() const bool workerStateValid = finalizeWrittenAssetWork(result, ready); if (!workerStateValid) { + if (m_runtime.mode == RunMode::CI) + failExit("Background written asset preparation is unavailable."); if (!writeAssetRoot(m_render.currentCpuAsset, m_output.writtenPath.string())) failExit("Geometry write failed."); From d18b6d9d60351ca9806ff0dc60d472525843d61c Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Wed, 11 Mar 2026 11:26:00 +0100 Subject: [PATCH 28/34] Add mesh loaders perf artifacts --- 12_MeshLoaders/App.hpp | 76 +++++ 12_MeshLoaders/AppLifecycle.cpp | 82 +++++- 12_MeshLoaders/AppLoad.cpp | 43 ++- 12_MeshLoaders/AppPerf.cpp | 479 ++++++++++++++++++++++++++++++++ 12_MeshLoaders/AppRuntime.cpp | 28 +- 12_MeshLoaders/CMakeLists.txt | 10 + 12_MeshLoaders/README.md | 27 ++ 7 files changed, 738 insertions(+), 7 deletions(-) create mode 100644 12_MeshLoaders/AppPerf.cpp diff --git a/12_MeshLoaders/App.hpp b/12_MeshLoaders/App.hpp index 01493f7dc..a78991380 100644 --- a/12_MeshLoaders/App.hpp +++ b/12_MeshLoaders/App.hpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -147,6 +148,49 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built } }; +public: + struct LoadStageMetrics + { + double getAssetMs = 0.0; + double extractMs = 0.0; + double totalMs = 0.0; + double nonLoaderMs = 0.0; + uintmax_t inputSize = 0u; + bool valid = false; + }; + + struct WriteStageMetrics + { + double openMs = 0.0; + double writeMs = 0.0; + double statMs = 0.0; + double totalMs = 0.0; + double nonWriterMs = 0.0; + uintmax_t outputSize = 0u; + bool usedMemoryTransport = false; + bool usedDiskFallback = false; + bool persistedDiskArtifact = false; + bool valid = false; + }; + + struct CasePerformanceMetrics + { + std::string caseName; + nbl::system::path inputPath; + LoadStageMetrics originalLoad = {}; + WriteStageMetrics write = {}; + LoadStageMetrics writtenLoad = {}; + }; + +private: + struct PerformanceOptions + { + std::optional dumpDir; + std::optional referenceDir; + std::optional profileOverride; + bool strict = false; + }; + struct WrittenAssetRequest { core::smart_refctd_ptr asset; @@ -169,6 +213,9 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built double totalWriteMs = 0.0; double nonWriterMs = 0.0; uintmax_t outputSize = 0u; + bool usedMemoryTransport = false; + bool usedDiskFallback = false; + bool persistedDiskArtifact = false; AssetLoadCallResult loadResult = {}; }; @@ -205,6 +252,22 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built bool stop = false; }; + struct PerformanceState + { + PerformanceOptions options = {}; + bool enabled = false; + bool finalized = false; + std::chrono::steady_clock::time_point runStart = {}; + size_t currentCaseIndex = ~size_t(0u); + std::string profileId; + std::string workloadId; + nbl::system::path dumpPath = {}; + nbl::system::path referencePath = {}; + bool referenceMatched = false; + core::vector comparisonFailures = {}; + core::vector completedCases = {}; + }; + struct RuntimeState { bool nonInteractiveTest = false; @@ -295,9 +358,12 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built void resetRowViewScene(); bool loadModel(const system::path& modelPath, bool updateCamera, bool storeCamera); + bool loadModel(const system::path& modelPath, bool updateCamera, bool storeCamera, LoadStageMetrics* perfMetrics); bool loadPreparedModel(const system::path& modelPath, AssetLoadCallResult&& loadResult, bool updateCamera, bool storeCamera); + bool loadPreparedModel(const system::path& modelPath, AssetLoadCallResult&& loadResult, bool updateCamera, bool storeCamera, LoadStageMetrics* perfMetrics); bool loadRowView(RowViewReloadMode mode); bool writeAssetRoot(smart_refctd_ptr asset, const std::string& savePath); + bool writeAssetRoot(smart_refctd_ptr asset, const std::string& savePath, WriteStageMetrics* perfMetrics); void setupCameraFromAABB(const hlsl::shapes::AABB<3, double>& bound); @@ -335,6 +401,15 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built void backgroundLoadWorkerMain(); bool startPreparedAssetLoad(size_t caseIndex, const system::path& path); bool finalizePreparedAssetLoad(PreparedAssetLoad& result, bool& ready, bool waitForCompletion=false); + bool performanceEnabled() const; + void beginPerformanceRun(); + void beginPerformanceCase(const TestCase& testCase); + void recordOriginalLoadMetrics(const LoadStageMetrics& metrics); + void recordWrittenLoadMetrics(const LoadStageMetrics& metrics); + void recordWriteMetrics(const WriteStageMetrics& metrics); + void recordWriteMetrics(const WrittenAssetResult& result); + void endPerformanceCase(); + void finalizePerformanceRun(); bool compareImages( const asset::ICPUImageView* a, const asset::ICPUImageView* b, @@ -357,6 +432,7 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built RowViewState m_rowView; BackgroundAssetWorker m_backgroundAssetWorker; BackgroundLoadWorker m_backgroundLoadWorker; + PerformanceState m_perf; InputSystem::ChannelReader mouse; InputSystem::ChannelReader keyboard; diff --git a/12_MeshLoaders/AppLifecycle.cpp b/12_MeshLoaders/AppLifecycle.cpp index dfddde5c4..796ce5102 100644 --- a/12_MeshLoaders/AppLifecycle.cpp +++ b/12_MeshLoaders/AppLifecycle.cpp @@ -51,6 +51,18 @@ void setupMeshLoadersArgumentParser(argparse::ArgumentParser& parser) parser.add_argument("--loader-perf-log") .nargs(1) .help("Write loader diagnostics to a file instead of stdout."); + parser.add_argument("--perf-dump-dir") + .nargs(1) + .help("Write structured performance run artifacts to this directory."); + parser.add_argument("--perf-ref-dir") + .nargs(1) + .help("Lookup directory for structured performance reference artifacts."); + parser.add_argument("--perf-strict") + .help("Fail the run if a matching structured performance reference exists and comparison exceeds thresholds.") + .flag(); + parser.add_argument("--perf-profile-override") + .nargs(1) + .help("Override the automatically generated performance profile id."); parser.add_argument("--loader-content-hashes") .help("Keep loader content hashes enabled. This is already the default for this example.") .flag(); @@ -107,6 +119,10 @@ struct ParsedCommandLineOptions system::path testListPath; std::optional specifiedGeomSavePath; std::optional loaderPerfLogPath; + std::optional perfDumpDir; + std::optional perfReferenceDir; + std::optional perfProfileOverride; + bool perfStrict = false; std::optional rowAddPath; uint32_t rowDuplicateCount = 0u; asset::SFileIOPolicy::SRuntimeTuning::Mode runtimeTuningMode = asset::SFileIOPolicy::SRuntimeTuning::Mode::Heuristic; @@ -369,6 +385,42 @@ bool parseMeshLoadersCommandLine( tmp = effectiveOutputCWD / tmp; out.loaderPerfLogPath = tmp; } + if (parser.present("--perf-dump-dir")) + { + auto tmp = path(parser.get("--perf-dump-dir")); + if (tmp.empty()) + { + error = "Invalid --perf-dump-dir value."; + return false; + } + if (tmp.is_relative()) + tmp = effectiveOutputCWD / tmp; + out.perfDumpDir = tmp; + } + if (parser.present("--perf-ref-dir")) + { + auto tmp = path(parser.get("--perf-ref-dir")); + if (tmp.empty()) + { + error = "Invalid --perf-ref-dir value."; + return false; + } + if (tmp.is_relative()) + tmp = effectiveOutputCWD / tmp; + out.perfReferenceDir = tmp; + } + if (parser["--perf-strict"] == true) + out.perfStrict = true; + if (parser.present("--perf-profile-override")) + { + const auto value = parser.get("--perf-profile-override"); + if (value.empty()) + { + error = "Invalid --perf-profile-override value."; + return false; + } + out.perfProfileOverride = value; + } if (parser["--loader-content-hashes"] == true) out.forceLoaderContentHashes = true; if (parser["--update-references"] == true) @@ -431,6 +483,11 @@ bool MeshLoadersApp::onAppInitialized(smart_refctd_ptr&& system) m_output.loaderPerfLogPath = std::move(options.loaderPerfLogPath); m_output.rowAddPath = std::move(options.rowAddPath); m_output.rowDuplicateCount = options.rowDuplicateCount; + m_perf.options.dumpDir = std::move(options.perfDumpDir); + m_perf.options.referenceDir = std::move(options.perfReferenceDir); + m_perf.options.profileOverride = std::move(options.perfProfileOverride); + m_perf.options.strict = options.perfStrict; + m_perf.enabled = m_perf.options.dumpDir.has_value() || m_perf.options.referenceDir.has_value() || m_perf.options.strict; m_forceLoaderContentHashes = options.forceLoaderContentHashes; m_updateGeometryHashReferences = options.updateGeometryHashReferences; m_runtimeTuningMode = options.runtimeTuningMode; @@ -489,6 +546,8 @@ bool MeshLoadersApp::onAppInitialized(smart_refctd_ptr&& system) if (!initTestCases()) return false; + if (performanceEnabled()) + beginPerformanceRun(); auto runInitialContent = [&]() -> bool { @@ -700,6 +759,11 @@ IQueue::SSubmitInfo::SSemaphoreInfo MeshLoadersApp::renderFrame(const std::chron bool MeshLoadersApp::onAppTerminated() { + if (performanceEnabled() && !m_perf.finalized) + { + endPerformanceCase(); + finalizePerformanceRun(); + } stopBackgroundLoadWorker(); stopBackgroundAssetWorker(); return device_base_t::onAppTerminated(); @@ -956,6 +1020,8 @@ bool MeshLoadersApp::startCase(const size_t index) resetCasePresentationState(); const auto& testCase = m_runtime.cases[m_runtime.caseIndex]; + if (performanceEnabled()) + beginPerformanceCase(testCase); const auto artifacts = makeCaseArtifacts( testCase.name, testCase.path, @@ -967,20 +1033,23 @@ bool MeshLoadersApp::startCase(const size_t index) m_output.writtenScreenshotPath = artifacts.writtenScreenshotPath; bool loaded = false; + LoadStageMetrics loadMetrics = {}; if (m_runtime.mode == RunMode::CI) { PreparedAssetLoad preparedLoad = {}; bool preparedReady = false; const bool loadWorkerStateValid = finalizePreparedAssetLoad(preparedLoad, preparedReady, true); if (loadWorkerStateValid && preparedReady && preparedLoad.success && preparedLoad.caseIndex == index && preparedLoad.path == testCase.path) - loaded = loadPreparedModel(testCase.path, std::move(preparedLoad.loadResult), true, true); + loaded = loadPreparedModel(testCase.path, std::move(preparedLoad.loadResult), true, true, &loadMetrics); else - loaded = loadModel(testCase.path, true, true); + loaded = loadModel(testCase.path, true, true, &loadMetrics); } else - loaded = loadModel(testCase.path, true, true); + loaded = loadModel(testCase.path, true, true, &loadMetrics); if (!loaded) return false; + if (performanceEnabled() && loadMetrics.valid) + recordOriginalLoadMetrics(loadMetrics); if (m_runtime.mode != RunMode::Interactive && m_output.saveGeom && m_render.currentCpuAsset) { @@ -1006,9 +1075,16 @@ bool MeshLoadersApp::advanceToNextCase() const auto nextIndex = m_runtime.caseIndex + 1u; if (nextIndex >= m_runtime.cases.size()) { + if (performanceEnabled()) + { + endPerformanceCase(); + finalizePerformanceRun(); + } m_runtime.shouldQuit = true; return false; } + if (performanceEnabled()) + endPerformanceCase(); if (!startCase(nextIndex)) { m_runtime.shouldQuit = true; diff --git a/12_MeshLoaders/AppLoad.cpp b/12_MeshLoaders/AppLoad.cpp index ebf6dad86..2fe26cea5 100644 --- a/12_MeshLoaders/AppLoad.cpp +++ b/12_MeshLoaders/AppLoad.cpp @@ -266,6 +266,11 @@ static void collectGeometryAABBs( } bool MeshLoadersApp::loadModel(const system::path& modelPath, bool updateCamera, bool storeCamera) +{ + return loadModel(modelPath, updateCamera, storeCamera, nullptr); +} + +bool MeshLoadersApp::loadModel(const system::path& modelPath, bool updateCamera, bool storeCamera, LoadStageMetrics* const perfMetrics) { if (modelPath.empty()) failExit("Empty model path."); @@ -275,10 +280,15 @@ bool MeshLoadersApp::loadModel(const system::path& modelPath, bool updateCamera, AssetLoadCallResult loadResult = {}; if (!loadAssetCallFromPath(modelPath, params, loadResult)) failExit("Failed to open input file %s.", modelPath.string().c_str()); - return loadPreparedModel(modelPath, std::move(loadResult), updateCamera, storeCamera); + return loadPreparedModel(modelPath, std::move(loadResult), updateCamera, storeCamera, perfMetrics); } bool MeshLoadersApp::loadPreparedModel(const system::path& modelPath, AssetLoadCallResult&& loadResult, bool updateCamera, bool storeCamera) +{ + return loadPreparedModel(modelPath, std::move(loadResult), updateCamera, storeCamera, nullptr); +} + +bool MeshLoadersApp::loadPreparedModel(const system::path& modelPath, AssetLoadCallResult&& loadResult, bool updateCamera, bool storeCamera, LoadStageMetrics* const perfMetrics) { using clock_t = std::chrono::high_resolution_clock; @@ -319,6 +329,17 @@ bool MeshLoadersApp::loadPreparedModel(const system::path& modelPath, AssetLoadC extractMs, outerMs, nonLoaderMs); + if (perfMetrics) + { + *perfMetrics = LoadStageMetrics{ + .getAssetMs = loadMs, + .extractMs = extractMs, + .totalMs = outerMs, + .nonLoaderMs = nonLoaderMs, + .inputSize = loadResult.inputSize, + .valid = true + }; + } m_render.currentCpuGeom = batch.geometries[0]; @@ -759,6 +780,11 @@ bool MeshLoadersApp::loadRowView(const RowViewReloadMode mode) } bool MeshLoadersApp::writeAssetRoot(smart_refctd_ptr asset, const std::string& savePath) +{ + return writeAssetRoot(std::move(asset), savePath, nullptr); +} + +bool MeshLoadersApp::writeAssetRoot(smart_refctd_ptr asset, const std::string& savePath, WriteStageMetrics* const perfMetrics) { using clock_t = std::chrono::high_resolution_clock; const auto writeOuterStart = clock_t::now(); @@ -811,6 +837,21 @@ bool MeshLoadersApp::writeAssetRoot(smart_refctd_ptr asset, const static_cast(size)); m_logger->log("Writer perf: path=%s ext=%s time=%.3f ms size=%llu", ILogger::ELL_INFO, savePath.c_str(), ext.c_str(), writeMs, static_cast(size)); m_logger->log("Mesh successfully saved!", ILogger::ELL_INFO); + if (perfMetrics) + { + *perfMetrics = WriteStageMetrics{ + .openMs = openMs, + .writeMs = writeMs, + .statMs = statMs, + .totalMs = outerMs, + .nonWriterMs = nonWriterMs, + .outputSize = size, + .usedMemoryTransport = false, + .usedDiskFallback = false, + .persistedDiskArtifact = true, + .valid = true + }; + } return true; } diff --git a/12_MeshLoaders/AppPerf.cpp b/12_MeshLoaders/AppPerf.cpp new file mode 100644 index 000000000..15d10fed2 --- /dev/null +++ b/12_MeshLoaders/AppPerf.cpp @@ -0,0 +1,479 @@ +// Copyright (C) 2018-2025 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#include "App.hpp" + +#include "nlohmann/json.hpp" +#include "nbl/core/hash/blake.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +using perf_json_t = nlohmann::ordered_json; + +constexpr std::string_view PerfSchemaVersion = "meshloaders-perf-run-v1"; +constexpr std::string_view PerfProtocolVersion = "meshloaders-roundtrip-v1"; + +std::string normalizeProfileComponent(std::string_view value) +{ + std::string normalized; + normalized.reserve(value.size()); + bool lastWasSeparator = false; + for (const auto ch : value) + { + if (std::isalnum(static_cast(ch))) + { + normalized.push_back(static_cast(std::tolower(static_cast(ch)))); + lastWasSeparator = false; + } + else if (!lastWasSeparator) + { + normalized.push_back('-'); + lastWasSeparator = true; + } + } + while (!normalized.empty() && normalized.back() == '-') + normalized.pop_back(); + if (normalized.empty()) + normalized = "unknown"; + return normalized; +} + +std::string hashToHex(const nbl::core::blake3_hash_t& hash) +{ + std::ostringstream oss; + oss << std::hex << std::setfill('0'); + for (const auto byte : hash.data) + oss << std::setw(2) << static_cast(byte); + return oss.str(); +} + +std::string currentTimestampTag() +{ + const auto now = std::chrono::system_clock::now(); + const auto time = std::chrono::system_clock::to_time_t(now); + std::tm tm = {}; +#ifdef _WIN32 + gmtime_s(&tm, &time); +#else + gmtime_r(&time, &tm); +#endif + std::ostringstream oss; + oss << std::put_time(&tm, "%Y%m%d_%H%M%S"); + return oss.str(); +} + +std::string runModeName(const uint32_t modeValue) +{ + switch (modeValue) + { + case 0u: + return "interactive"; + case 1u: + return "batch"; + case 2u: + return "ci"; + default: + return "unknown"; + } +} + +std::string runtimeTuningModeName(const asset::SFileIOPolicy::SRuntimeTuning::Mode mode) +{ + switch (mode) + { + case asset::SFileIOPolicy::SRuntimeTuning::Mode::Sequential: + return "sequential"; + case asset::SFileIOPolicy::SRuntimeTuning::Mode::Heuristic: + return "heuristic"; + case asset::SFileIOPolicy::SRuntimeTuning::Mode::Hybrid: + return "hybrid"; + default: + return "unknown"; + } +} + +std::string platformName() +{ +#if defined(_WIN32) + return "windows"; +#elif defined(__linux__) + return "linux"; +#elif defined(__ANDROID__) + return "android"; +#else + return "unknown"; +#endif +} + +std::string buildConfigName() +{ +#if defined(NBL_MESHLOADERS_BUILD_CONFIG) + return NBL_MESHLOADERS_BUILD_CONFIG; +#elif defined(_DEBUG) + return "Debug"; +#else + return "Release"; +#endif +} + +std::filesystem::path normalizePathForPerf(const std::filesystem::path& path) +{ + if (path.empty()) + return {}; + + std::error_code ec; + auto normalized = path.lexically_normal(); + if (std::filesystem::exists(normalized, ec) && !ec) + { + auto canonical = std::filesystem::weakly_canonical(normalized, ec); + if (!ec) + return canonical.lexically_normal(); + ec.clear(); + } + else + ec.clear(); + + if (!normalized.is_absolute()) + { + auto absolute = std::filesystem::absolute(normalized, ec); + if (!ec) + return absolute.lexically_normal(); + ec.clear(); + } + + return normalized; +} + +std::optional tryRelativePerfPath(const std::filesystem::path& targetPath, const std::filesystem::path& basePath) +{ + if (targetPath.empty() || basePath.empty()) + return std::nullopt; + + std::error_code ec; + const auto normalizedTarget = normalizePathForPerf(targetPath); + const auto normalizedBase = normalizePathForPerf(basePath); + const auto relative = std::filesystem::relative(normalizedTarget, normalizedBase, ec); + if (ec || relative.empty() || relative.is_absolute()) + return std::nullopt; + + return relative.lexically_normal().generic_string(); +} + +std::string makePortablePerfPath(const std::filesystem::path& path, const std::optional& preferredBase = std::nullopt) +{ + if (path.empty()) + return {}; + + auto normalized = path.lexically_normal(); + if (!normalized.is_absolute()) + return normalized.generic_string(); + + if (preferredBase.has_value()) + if (auto relative = tryRelativePerfPath(path, *preferredBase); relative.has_value()) + return *relative; + + std::error_code ec; + const auto cwd = std::filesystem::current_path(ec); + if (!ec) + if (auto relative = tryRelativePerfPath(path, cwd); relative.has_value()) + return *relative; + + normalized = normalizePathForPerf(path); + if (!normalized.filename().empty()) + return normalized.filename().generic_string(); + + return normalizeProfileComponent(normalized.generic_string()); +} + +perf_json_t toJson(const MeshLoadersApp::LoadStageMetrics& metrics) +{ + return perf_json_t{ + {"valid", metrics.valid}, + {"input_size", metrics.inputSize}, + {"get_asset_ms", metrics.getAssetMs}, + {"extract_ms", metrics.extractMs}, + {"total_ms", metrics.totalMs}, + {"non_loader_ms", metrics.nonLoaderMs} + }; +} + +perf_json_t toJson(const MeshLoadersApp::WriteStageMetrics& metrics) +{ + return perf_json_t{ + {"valid", metrics.valid}, + {"output_size", metrics.outputSize}, + {"open_ms", metrics.openMs}, + {"write_ms", metrics.writeMs}, + {"stat_ms", metrics.statMs}, + {"total_ms", metrics.totalMs}, + {"non_writer_ms", metrics.nonWriterMs}, + {"used_memory_transport", metrics.usedMemoryTransport}, + {"used_disk_fallback", metrics.usedDiskFallback}, + {"persisted_disk_artifact", metrics.persistedDiskArtifact} + }; +} + +bool metricRegression(const double current, const double reference, const double relativeThreshold, const double absoluteThreshold) +{ + if (reference <= 0.0) + return current > absoluteThreshold; + const double allowed = std::max(reference * (1.0 + relativeThreshold), reference + absoluteThreshold); + return current > allowed; +} + +void compareMetric(core::vector& failures, const std::string& caseName, const std::string_view metricName, const double current, const double reference, const double relativeThreshold, const double absoluteThreshold) +{ + if (!metricRegression(current, reference, relativeThreshold, absoluteThreshold)) + return; + + std::ostringstream oss; + oss << caseName << ": " << metricName << " regressed from " << reference << " ms to " << current << " ms"; + failures.push_back(oss.str()); +} + +perf_json_t buildCaseJson(const MeshLoadersApp::CasePerformanceMetrics& metrics, const std::optional& testListDir) +{ + return perf_json_t{ + {"name", metrics.caseName}, + {"input_path", makePortablePerfPath(metrics.inputPath, testListDir)}, + {"original_load", toJson(metrics.originalLoad)}, + {"write", toJson(metrics.write)}, + {"written_load", toJson(metrics.writtenLoad)} + }; +} + +} + +bool MeshLoadersApp::performanceEnabled() const +{ + return m_perf.enabled; +} + +void MeshLoadersApp::beginPerformanceRun() +{ + if (!performanceEnabled()) + return; + + m_perf.finalized = false; + m_perf.completedCases.clear(); + m_perf.completedCases.reserve(m_runtime.cases.size()); + m_perf.comparisonFailures.clear(); + m_perf.runStart = std::chrono::steady_clock::now(); + + const auto systemInfo = m_system->getSystemInfo(); + const auto normalizedCpuName = normalizeProfileComponent(systemInfo.cpuName); + if (m_perf.options.profileOverride.has_value()) + m_perf.profileId = normalizeProfileComponent(*m_perf.options.profileOverride); + else + { + std::ostringstream profile; + profile << platformName() + << "__" << normalizedCpuName + << "__thr-" << std::thread::hardware_concurrency() + << "__" << normalizeProfileComponent(buildConfigName()); + m_perf.profileId = normalizeProfileComponent(profile.str()); + } + + nbl::core::blake3_hasher workloadHasher; + workloadHasher.update(PerfProtocolVersion.data(), PerfProtocolVersion.size()); + workloadHasher << static_cast(m_runtime.mode); + workloadHasher << static_cast(m_runtimeTuningMode); + workloadHasher << m_runtime.rowViewEnabled; + if (!m_output.testListPath.empty() && std::filesystem::exists(m_output.testListPath)) + { + std::ifstream stream(m_output.testListPath, std::ios::binary); + std::string content((std::istreambuf_iterator(stream)), std::istreambuf_iterator()); + workloadHasher.update(content.data(), content.size()); + } + else + { + workloadHasher << m_runtime.cases.size(); + for (const auto& testCase : m_runtime.cases) + { + workloadHasher << testCase.name; + workloadHasher << makePortablePerfPath(testCase.path); + } + } + m_perf.workloadId = hashToHex(static_cast(workloadHasher)); +} + +void MeshLoadersApp::beginPerformanceCase(const TestCase& testCase) +{ + if (!performanceEnabled()) + return; + + m_perf.currentCaseIndex = m_perf.completedCases.size(); + m_perf.completedCases.push_back(CasePerformanceMetrics{ + .caseName = testCase.name, + .inputPath = testCase.path + }); +} + +void MeshLoadersApp::recordOriginalLoadMetrics(const LoadStageMetrics& metrics) +{ + if (!performanceEnabled() || m_perf.currentCaseIndex >= m_perf.completedCases.size()) + return; + m_perf.completedCases[m_perf.currentCaseIndex].originalLoad = metrics; +} + +void MeshLoadersApp::recordWrittenLoadMetrics(const LoadStageMetrics& metrics) +{ + if (!performanceEnabled() || m_perf.currentCaseIndex >= m_perf.completedCases.size()) + return; + m_perf.completedCases[m_perf.currentCaseIndex].writtenLoad = metrics; +} + +void MeshLoadersApp::recordWriteMetrics(const WriteStageMetrics& metrics) +{ + if (!performanceEnabled() || m_perf.currentCaseIndex >= m_perf.completedCases.size()) + return; + m_perf.completedCases[m_perf.currentCaseIndex].write = metrics; +} + +void MeshLoadersApp::recordWriteMetrics(const WrittenAssetResult& result) +{ + WriteStageMetrics metrics = {}; + metrics.openMs = result.openMs; + metrics.writeMs = result.writeMs; + metrics.statMs = result.statMs; + metrics.totalMs = result.totalWriteMs; + metrics.nonWriterMs = result.nonWriterMs; + metrics.outputSize = result.outputSize; + metrics.usedMemoryTransport = result.usedMemoryTransport; + metrics.usedDiskFallback = result.usedDiskFallback; + metrics.persistedDiskArtifact = result.persistedDiskArtifact; + metrics.valid = true; + recordWriteMetrics(metrics); +} + +void MeshLoadersApp::endPerformanceCase() +{ + if (!performanceEnabled() || m_perf.currentCaseIndex >= m_perf.completedCases.size()) + return; + + m_perf.currentCaseIndex = ~size_t(0u); +} + +void MeshLoadersApp::finalizePerformanceRun() +{ + if (!performanceEnabled() || m_perf.finalized) + return; + + m_perf.finalized = true; + + perf_json_t root = {}; + root["schema_version"] = PerfSchemaVersion; + root["protocol_version"] = PerfProtocolVersion; + root["profile_id"] = m_perf.profileId; + root["workload_id"] = m_perf.workloadId; + root["run_mode"] = runModeName(static_cast(m_runtime.mode)); + root["runtime_tuning"] = runtimeTuningModeName(m_runtimeTuningMode); + + const auto systemInfo = m_system->getSystemInfo(); + root["environment"] = { + {"platform", platformName()}, + {"os_full_name", systemInfo.OSFullName}, + {"cpu_name", systemInfo.cpuName}, + {"physical_core_count", systemInfo.physicalCoreCount}, + {"gpu_name", m_physicalDevice ? m_physicalDevice->getProperties().deviceName : "unknown"}, + {"total_memory_bytes", systemInfo.totalMemory}, + {"available_memory_bytes", systemInfo.availableMemory}, + {"hardware_concurrency", std::thread::hardware_concurrency()}, + {"build_config", buildConfigName()} + }; + const auto testListDir = m_output.testListPath.empty() ? std::optional{} : std::optional{m_output.testListPath.parent_path()}; + const auto testListName = m_output.testListPath.empty() ? std::string{} : m_output.testListPath.filename().generic_string(); + root["inputs"] = { + {"test_list_name", testListName}, + {"row_view_enabled", m_runtime.rowViewEnabled}, + {"case_count", m_runtime.cases.size()} + }; + + root["cases"] = perf_json_t::array(); + for (const auto& metrics : m_perf.completedCases) + root["cases"].push_back(buildCaseJson(metrics, testListDir)); + root["totals"] = { + {"run_wall_ms", toMs(std::chrono::steady_clock::now() - m_perf.runStart)} + }; + + if (m_perf.options.referenceDir) + { + m_perf.referencePath = *m_perf.options.referenceDir / m_perf.workloadId / (m_perf.profileId + ".json"); + root["reference"]["lookup_key"] = m_perf.workloadId + "/" + m_perf.profileId + ".json"; + if (std::filesystem::exists(m_perf.referencePath)) + { + m_perf.referenceMatched = true; + std::ifstream stream(m_perf.referencePath); + perf_json_t reference; + stream >> reference; + + if (!reference.contains("cases") || !reference["cases"].is_array()) + m_perf.comparisonFailures.push_back("Reference file does not contain a valid cases array."); + else + { + std::unordered_map referenceCases; + for (const auto& caseJson : reference["cases"]) + referenceCases.emplace(caseJson.value("name", ""), caseJson); + + if (referenceCases.size() != m_perf.completedCases.size()) + m_perf.comparisonFailures.push_back("Reference case count does not match the current run."); + + for (const auto& metrics : m_perf.completedCases) + { + const auto refIt = referenceCases.find(metrics.caseName); + if (refIt == referenceCases.end()) + { + m_perf.comparisonFailures.push_back(metrics.caseName + ": missing reference case."); + continue; + } + + const auto& refCase = refIt->second; + compareMetric(m_perf.comparisonFailures, metrics.caseName, "original_load.total_ms", metrics.originalLoad.totalMs, refCase["original_load"].value("total_ms", 0.0), 0.20, 5.0); + compareMetric(m_perf.comparisonFailures, metrics.caseName, "write.total_ms", metrics.write.totalMs, refCase["write"].value("total_ms", 0.0), 0.20, 5.0); + compareMetric(m_perf.comparisonFailures, metrics.caseName, "written_load.total_ms", metrics.writtenLoad.totalMs, refCase["written_load"].value("total_ms", 0.0), 0.20, 5.0); + + const bool refUsedMemoryTransport = refCase["write"].value("used_memory_transport", false); + if (metrics.write.valid && metrics.write.usedMemoryTransport != refUsedMemoryTransport) + m_perf.comparisonFailures.push_back(metrics.caseName + ": memory transport usage does not match the reference."); + } + } + } + } + root["reference"]["matched"] = m_perf.referenceMatched; + root["reference"]["strict"] = m_perf.options.strict; + root["reference"]["comparison_failures"] = m_perf.comparisonFailures; + + if (m_perf.options.dumpDir) + { + const auto dumpDir = *m_perf.options.dumpDir / m_perf.workloadId; + std::filesystem::create_directories(dumpDir); + m_perf.dumpPath = dumpDir / (currentTimestampTag() + "__" + m_perf.profileId + ".json"); + std::ofstream out(m_perf.dumpPath); + out << root.dump(2); + } + + if (m_logger) + { + if (!m_perf.referenceMatched) + m_logger->log("Performance reference not found for workload=%s profile=%s.", ILogger::ELL_INFO, m_perf.workloadId.c_str(), m_perf.profileId.c_str()); + else if (m_perf.comparisonFailures.empty()) + m_logger->log("Performance reference comparison passed for workload=%s profile=%s.", ILogger::ELL_INFO, m_perf.workloadId.c_str(), m_perf.profileId.c_str()); + else + for (const auto& failure : m_perf.comparisonFailures) + m_logger->log("%s", ILogger::ELL_ERROR, failure.c_str()); + } + + if (m_perf.options.strict && m_perf.referenceMatched && !m_perf.comparisonFailures.empty()) + failExit("Structured performance comparison failed."); +} diff --git a/12_MeshLoaders/AppRuntime.cpp b/12_MeshLoaders/AppRuntime.cpp index 4394487db..f8a970cc0 100644 --- a/12_MeshLoaders/AppRuntime.cpp +++ b/12_MeshLoaders/AppRuntime.cpp @@ -230,6 +230,7 @@ void MeshLoadersApp::backgroundAssetWorkerMain() writeParams.logger = request.loadParams.logger; bool useDiskTransport = !request.useMemoryTransport; + result.usedMemoryTransport = request.useMemoryTransport; if (request.useMemoryTransport) { auto memoryFile = core::make_smart_refctd_ptr(system::path(request.path)); @@ -273,7 +274,10 @@ void MeshLoadersApp::backgroundAssetWorkerMain() if (!persistMemoryFileToDisk(workerSystem.get(), request.path, memoryFile.get())) { if (request.allowDiskFallback) + { useDiskTransport = true; + result.usedDiskFallback = true; + } else result.error = "Background asset worker failed to persist the in-memory written asset."; if (useDiskTransport) @@ -284,12 +288,17 @@ void MeshLoadersApp::backgroundAssetWorkerMain() result.nonWriterMs = 0.0; } } + else + result.persistedDiskArtifact = true; } } else { if (request.allowDiskFallback) + { useDiskTransport = true; + result.usedDiskFallback = true; + } else result.error = "Background asset worker could not create the in-memory transport."; } @@ -297,6 +306,8 @@ void MeshLoadersApp::backgroundAssetWorkerMain() if (useDiskTransport && result.error.empty()) { + result.usedDiskFallback = result.usedDiskFallback || request.useMemoryTransport; + result.persistedDiskArtifact = true; const auto openStart = clock_t::now(); system::ISystem::future_t> writeFileFuture; workerSystem->createFile(writeFileFuture, request.path, system::IFile::ECF_WRITE); @@ -793,11 +804,16 @@ void MeshLoadersApp::advanceCase() if (!result.success) failExit("%s", result.error.c_str()); logWrittenAssetWork(result); + if (performanceEnabled()) + recordWriteMetrics(result); if (m_runtime.mode == RunMode::CI) { - if (!loadPreparedModel(m_output.writtenPath, std::move(result.loadResult), false, false)) + LoadStageMetrics writtenLoadMetrics = {}; + if (!loadPreparedModel(m_output.writtenPath, std::move(result.loadResult), false, false, &writtenLoadMetrics)) failExit("Failed to load written asset %s.", m_output.writtenPath.string().c_str()); + if (performanceEnabled() && writtenLoadMetrics.valid) + recordWrittenLoadMetrics(writtenLoadMetrics); if (!m_render.currentCpuGeom) failExit("Written geometry missing."); m_runtime.phase = Phase::RenderWritten; @@ -882,13 +898,19 @@ void MeshLoadersApp::advanceCase() { if (m_runtime.mode == RunMode::CI) failExit("Background written asset preparation is unavailable."); - if (!writeAssetRoot(m_render.currentCpuAsset, m_output.writtenPath.string())) + WriteStageMetrics writeMetrics = {}; + if (!writeAssetRoot(m_render.currentCpuAsset, m_output.writtenPath.string(), &writeMetrics)) failExit("Geometry write failed."); + if (performanceEnabled() && writeMetrics.valid) + recordWriteMetrics(writeMetrics); if (m_runtime.mode == RunMode::CI) { - if (!loadModel(m_output.writtenPath, false, false)) + LoadStageMetrics writtenLoadMetrics = {}; + if (!loadModel(m_output.writtenPath, false, false, &writtenLoadMetrics)) failExit("Failed to load written asset %s.", m_output.writtenPath.string().c_str()); + if (performanceEnabled() && writtenLoadMetrics.valid) + recordWrittenLoadMetrics(writtenLoadMetrics); if (!m_render.currentCpuGeom) failExit("Written geometry missing."); m_runtime.phase = Phase::RenderWritten; diff --git a/12_MeshLoaders/CMakeLists.txt b/12_MeshLoaders/CMakeLists.txt index 515456ebb..b404f7bfc 100644 --- a/12_MeshLoaders/CMakeLists.txt +++ b/12_MeshLoaders/CMakeLists.txt @@ -4,6 +4,7 @@ set(SRCs BundleGeometryItems.h AppLifecycle.cpp AppLoad.cpp + AppPerf.cpp AppRuntime.cpp inputs.json README.md @@ -15,6 +16,8 @@ set(NBL_MESHLOADERS_BENCHMARK_DATASET_DIR "${CMAKE_BINARY_DIR}/meshloaders_bench set(NBL_MESHLOADERS_BENCHMARK_DATASET_REPO "https://github.com/Devsh-Graphics-Programming/Nabla-Benchmark-Datasets.git" CACHE STRING "Git repository URL for 12_MeshLoaders benchmark datasets.") set(NBL_MESHLOADERS_BENCHMARK_PAYLOAD_RELATIVE_PATH "inputs_benchmark.json" CACHE STRING "Relative path to committed benchmark payload JSON inside dataset repo.") set(NBL_MESHLOADERS_MEDIA_PAYLOAD_RELATIVE_PATH "inputs_benchmark.json" CACHE STRING "Preferred benchmark payload relative path inside examples_tests/media.") +set(NBL_MESHLOADERS_PERF_DUMP_DIR "${CMAKE_BINARY_DIR}/meshloaders_perf_runs" CACHE PATH "Directory where 12_MeshLoaders writes structured performance run artifacts.") +set(NBL_MESHLOADERS_PERF_REFERENCE_DIR "" CACHE PATH "Directory containing structured performance reference artifacts for 12_MeshLoaders.") set(NBL_INCLUDE_SEARCH_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}/include" @@ -34,6 +37,7 @@ if (NBL_BUILD_MITSUBA_LOADER) endif() nbl_create_executable_project("${SRCs}" "" "${NBL_INCLUDE_SEARCH_DIRECTORIES}" "${NBL_LIBRARIES}") +target_compile_definitions(${EXECUTABLE_NAME} PRIVATE NBL_MESHLOADERS_BUILD_CONFIG="$") if (NBL_BUILD_DEBUG_DRAW) target_link_libraries(${EXECUTABLE_NAME} PRIVATE Nabla::ext::DebugDraw) @@ -83,6 +87,9 @@ if (NBL_MESHLOADERS_ENABLE_BENCHMARK_DATASETS) message(FATAL_ERROR "Benchmark payload JSON must be a normal Git file, not an LFS pointer: ${NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON}") endif () message(STATUS "[meshloaders-bench] Benchmark inputs payload: ${NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON}") + if (NBL_MESHLOADERS_PERF_REFERENCE_DIR STREQUAL "") + set(NBL_MESHLOADERS_PERF_REFERENCE_DIR "${NBL_MESHLOADERS_BENCHMARK_DATASET_DIR}/meshloaders/perf_refs" CACHE PATH "Directory containing structured performance reference artifacts for 12_MeshLoaders." FORCE) + endif() if (NBL_MESHLOADERS_DEFAULT_START_WITH_BENCHMARK_TESTLIST) file(TO_CMAKE_PATH "${NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON}" _NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON_CMAKE) target_compile_definitions(${EXECUTABLE_NAME} PRIVATE NBL_MESHLOADERS_DEFAULT_BENCHMARK_TESTLIST_PATH="${_NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON_CMAKE}") @@ -105,6 +112,9 @@ if (NBL_MESHLOADERS_ENABLE_BENCHMARK_DATASETS) --ci --testlist "${NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON}" --loader-content-hashes + --perf-dump-dir "${NBL_MESHLOADERS_PERF_DUMP_DIR}" + --perf-ref-dir "${NBL_MESHLOADERS_PERF_REFERENCE_DIR}" + --perf-strict ) add_test(NAME NBL_MESHLOADERS_CI_BENCHMARK_HEURISTIC diff --git a/12_MeshLoaders/README.md b/12_MeshLoaders/README.md index b046bcf91..4f9b04746 100644 --- a/12_MeshLoaders/README.md +++ b/12_MeshLoaders/README.md @@ -56,6 +56,8 @@ Example for loading and writing `OBJ`, `PLY` and `STL` meshes. - Run benchmark CI directly via `ctest`: - `ctest --output-on-failure -C Debug -R NBL_MESHLOADERS_CI_BENCHMARK` - runs both benchmark CI modes: `heuristic` and `hybrid` + - when benchmark datasets are enabled, benchmark `ctest` also writes structured performance run artifacts + - if a matching reference exists for the current workload and machine profile, strict comparison is enabled automatically - Run default CI directly via `ctest` (no benchmark datasets enabled): - `ctest --output-on-failure -C Debug -R ^NBL_MESHLOADERS_CI$` - uses default `inputs.json` (3 inputs) @@ -85,6 +87,15 @@ Example for loading and writing `OBJ`, `PLY` and `STL` meshes. - this is already the default for this example - `--runtime-tuning ` - IO runtime tuning mode +- `--perf-dump-dir ` + - write structured performance run JSON artifacts +- `--perf-ref-dir ` + - lookup directory for structured performance references +- `--perf-strict` + - fail on performance regression only when a matching reference exists + - if no matching reference exists, the run stays record-only +- `--perf-profile-override ` + - override the automatically derived machine profile id ## Controls (non-CI) - Arrow keys: move camera @@ -119,6 +130,22 @@ Rules: - thresholds come from `MaxImageDiffCodeUnits` and `MaxImageDiffCodeUnitValue` in `App.hpp` - Any mismatch ends with non-zero exit code +## Structured Perf Runs +- Structured performance output is keyed by: + - `workload_id` + - derived from the benchmark/test input definition and runtime mode + - `profile_id` + - derived from the current CPU-centric machine/runtime profile or overridden explicitly +- Reference lookup uses: + - `//.json` +- If no matching reference exists: + - no comparison is performed + - the run only writes its current JSON artifact +- If a matching reference exists: + - per-case `original_load`, `write`, and `written_load` stage metrics are compared + - strict mode fails only on actual regression, not on missing references +- JSON artifacts avoid host-specific absolute paths and store portable case/test-list identifiers instead + ## Performance logs to trust - `Asset load call perf` for `getAsset` - `Asset write call perf` for `writeAsset` From 530edd61f9634b66fb7059e2cb908639d36d40b8 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Wed, 11 Mar 2026 11:49:29 +0100 Subject: [PATCH 29/34] Use git provenance in mesh perf --- 12_MeshLoaders/AppPerf.cpp | 31 +++++++++++++++++++++++++++++++ 12_MeshLoaders/README.md | 6 ++++++ 2 files changed, 37 insertions(+) diff --git a/12_MeshLoaders/AppPerf.cpp b/12_MeshLoaders/AppPerf.cpp index 15d10fed2..1dd4db37b 100644 --- a/12_MeshLoaders/AppPerf.cpp +++ b/12_MeshLoaders/AppPerf.cpp @@ -4,6 +4,8 @@ #include "App.hpp" +#include "git_info.h" + #include "nlohmann/json.hpp" #include "nbl/core/hash/blake.h" @@ -73,6 +75,21 @@ std::string currentTimestampTag() return oss.str(); } +std::string currentTimestampIsoUtc() +{ + const auto now = std::chrono::system_clock::now(); + const auto time = std::chrono::system_clock::to_time_t(now); + std::tm tm = {}; +#ifdef _WIN32 + gmtime_s(&tm, &time); +#else + gmtime_r(&time, &tm); +#endif + std::ostringstream oss; + oss << std::put_time(&tm, "%Y-%m-%dT%H:%M:%SZ"); + return oss.str(); +} + std::string runModeName(const uint32_t modeValue) { switch (modeValue) @@ -127,6 +144,13 @@ std::string buildConfigName() #endif } +perf_json_t dirtyStateJson(const std::optional& dirty) +{ + if (!dirty.has_value()) + return nullptr; + return perf_json_t(dirty.value()); +} + std::filesystem::path normalizePathForPerf(const std::filesystem::path& path) { if (path.empty()) @@ -379,6 +403,13 @@ void MeshLoadersApp::finalizePerformanceRun() root["workload_id"] = m_perf.workloadId; root["run_mode"] = runModeName(static_cast(m_runtime.mode)); root["runtime_tuning"] = runtimeTuningModeName(m_runtimeTuningMode); + root["provenance"] = { + {"created_at_utc", currentTimestampIsoUtc()}, + {"nabla_commit", gtml::nabla_git_info.commitHash}, + {"nabla_dirty", dirtyStateJson(gtml::nabla_git_info.hasUncommittedChanges)}, + {"examples_commit", gtml::examples_git_info.commitHash}, + {"examples_dirty", dirtyStateJson(gtml::examples_git_info.hasUncommittedChanges)} + }; const auto systemInfo = m_system->getSystemInfo(); root["environment"] = { diff --git a/12_MeshLoaders/README.md b/12_MeshLoaders/README.md index 4f9b04746..a7dc56362 100644 --- a/12_MeshLoaders/README.md +++ b/12_MeshLoaders/README.md @@ -136,6 +136,12 @@ Rules: - derived from the benchmark/test input definition and runtime mode - `profile_id` - derived from the current CPU-centric machine/runtime profile or overridden explicitly +- Structured performance artifacts also carry provenance: + - `created_at_utc` + - `nabla_commit` + - `nabla_dirty` + - `examples_commit` + - `examples_dirty` - Reference lookup uses: - `//.json` - If no matching reference exists: From 2aa2d144915aefd46fa7d9bfe7f68b9c3d37f7b7 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Wed, 11 Mar 2026 12:39:07 +0100 Subject: [PATCH 30/34] Enable examples git tracking --- CMakeLists.txt | 3 +++ common/CMakeLists.txt | 2 +- common/include/nbl/examples/PCH.hpp | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 32036cd93..99f2d80df 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,9 @@ if(NBL_BUILD_EXAMPLES) project(NablaExamples) enable_testing() + if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.git") + NBL_ADD_GIT_TRACKING_META_LIBRARY(examples "${CMAKE_CURRENT_SOURCE_DIR}") + endif() if(NBL_BUILD_ANDROID) nbl_android_create_media_storage_apk() diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index b3e57da6f..bd6db8cbc 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -106,4 +106,4 @@ set(NBL_EXAMPLES_API_TARGET ${LIB_NAME} PARENT_SCOPE) ]] set(NBL_EXAMPLES_API_LIBRARIES ${TARGETS} PARENT_SCOPE) -NBL_ADJUST_FOLDERS(common) \ No newline at end of file +NBL_ADJUST_FOLDERS(common) diff --git a/common/include/nbl/examples/PCH.hpp b/common/include/nbl/examples/PCH.hpp index a20984464..d03b9b74f 100644 --- a/common/include/nbl/examples/PCH.hpp +++ b/common/include/nbl/examples/PCH.hpp @@ -26,4 +26,4 @@ #include "nbl/examples/geometry/CSimpleDebugRenderer.hpp" -#endif // _NBL_EXAMPLES_COMMON_PCH_HPP_ \ No newline at end of file +#endif // _NBL_EXAMPLES_COMMON_PCH_HPP_ From 348eef714febe90da0195d89bc364df7a45320d5 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Wed, 11 Mar 2026 13:00:23 +0100 Subject: [PATCH 31/34] Ignore examples git check target --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 99f2d80df..a16bae025 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -119,6 +119,7 @@ if(NBL_BUILD_EXAMPLES) # add new examples *before* NBL_GET_ALL_TARGETS invocation, it gathers recursively all targets created so far in this subdirectory NBL_GET_ALL_TARGETS(TARGETS) + list(REMOVE_ITEM TARGETS examples_check_git) # we want to loop only over the examples so we exclude examples' interface libraries created in common subdirectory list(REMOVE_ITEM TARGETS ${NBL_EXAMPLES_API_TARGET} ${NBL_EXAMPLES_API_LIBRARIES}) From 87b1305d6504dade7d7c901d7ab5f9e71f1fbaa3 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Wed, 11 Mar 2026 17:25:39 +0100 Subject: [PATCH 32/34] Share examples git metadata target --- 12_MeshLoaders/AppPerf.cpp | 10 +++++----- 12_MeshLoaders/CMakeLists.txt | 1 - 40_PathTracer/main.cpp | 29 ++++------------------------- CMakeLists.txt | 17 ++++++++++++++--- 4 files changed, 23 insertions(+), 34 deletions(-) diff --git a/12_MeshLoaders/AppPerf.cpp b/12_MeshLoaders/AppPerf.cpp index 1dd4db37b..e91ef2417 100644 --- a/12_MeshLoaders/AppPerf.cpp +++ b/12_MeshLoaders/AppPerf.cpp @@ -4,7 +4,7 @@ #include "App.hpp" -#include "git_info.h" +#include "nbl/examples/git/info.h" #include "nlohmann/json.hpp" #include "nbl/core/hash/blake.h" @@ -405,10 +405,10 @@ void MeshLoadersApp::finalizePerformanceRun() root["runtime_tuning"] = runtimeTuningModeName(m_runtimeTuningMode); root["provenance"] = { {"created_at_utc", currentTimestampIsoUtc()}, - {"nabla_commit", gtml::nabla_git_info.commitHash}, - {"nabla_dirty", dirtyStateJson(gtml::nabla_git_info.hasUncommittedChanges)}, - {"examples_commit", gtml::examples_git_info.commitHash}, - {"examples_dirty", dirtyStateJson(gtml::examples_git_info.hasUncommittedChanges)} + {"nabla_commit", std::string(nbl::gtml::nabla_git_info.commitHash())}, + {"nabla_dirty", dirtyStateJson(nbl::gtml::nabla_git_info.hasUncommittedChanges())}, + {"examples_commit", std::string(nbl::examples::gtml::examples_git_info.commitHash())}, + {"examples_dirty", dirtyStateJson(nbl::examples::gtml::examples_git_info.hasUncommittedChanges())} }; const auto systemInfo = m_system->getSystemInfo(); diff --git a/12_MeshLoaders/CMakeLists.txt b/12_MeshLoaders/CMakeLists.txt index b404f7bfc..c6f6f3953 100644 --- a/12_MeshLoaders/CMakeLists.txt +++ b/12_MeshLoaders/CMakeLists.txt @@ -43,7 +43,6 @@ if (NBL_BUILD_DEBUG_DRAW) target_link_libraries(${EXECUTABLE_NAME} PRIVATE Nabla::ext::DebugDraw) endif() - add_dependencies(${EXECUTABLE_NAME} argparse) target_include_directories(${EXECUTABLE_NAME} PUBLIC $) diff --git a/40_PathTracer/main.cpp b/40_PathTracer/main.cpp index 8fd248f0a..fe374fef9 100644 --- a/40_PathTracer/main.cpp +++ b/40_PathTracer/main.cpp @@ -4,6 +4,7 @@ #include "nbl/examples/common/BuiltinResourcesApplication.hpp" #include "nbl/examples/examples.hpp" +#include "nbl/gtml/SJsonFormatter.h" #include "renderer/CRenderer.h" #include "renderer/resolve/CBasicRWMCResolver.h" @@ -34,35 +35,13 @@ class PathTracingApp final : public SimpleWindowedApplication, public BuiltinRes using device_base_t = SimpleWindowedApplication; using asset_base_t = BuiltinResourcesApplication; - // TODO: move to Nabla proper - static inline void jsonizeGitInfo(nlohmann::json& target, const nbl::gtml::GitInfo& info) - { - target["isPopulated"] = info.isPopulated; - if (info.hasUncommittedChanges.has_value()) - target["hasUncommittedChanges"] = info.hasUncommittedChanges.value(); - else - target["hasUncommittedChanges"] = "UNKNOWN, BUILT WITHOUT DIRTY-CHANGES CAPTURE"; - - target["commitAuthorName"] = info.commitAuthorName; - target["commitAuthorEmail"] = info.commitAuthorEmail; - target["commitHash"] = info.commitHash; - target["commitShortHash"] = info.commitShortHash; - target["commitDate"] = info.commitDate; - target["commitSubject"] = info.commitSubject; - target["commitBody"] = info.commitBody; - target["describe"] = info.describe; - target["branchName"] = info.branchName; - target["latestTag"] = info.latestTag; - target["latestTagName"] = info.latestTagName; - } - inline void printGitInfos() const { nlohmann::json j; auto& modules = j["modules"]; - jsonizeGitInfo(modules["nabla"],nbl::gtml::nabla_git_info); - jsonizeGitInfo(modules["dxc"],nbl::gtml::dxc_git_info); + modules["nabla"] = nlohmann::json::parse(::gtml::SJsonFormatter::toString(nbl::gtml::nabla_git_info)); + modules["dxc"] = nlohmann::json::parse(::gtml::SJsonFormatter::toString(nbl::gtml::dxc_git_info)); m_logger->log("Build Info:\n%s",ILogger::ELL_INFO,j.dump(4).c_str()); } @@ -590,4 +569,4 @@ class PathTracingApp final : public SimpleWindowedApplication, public BuiltinRes #endif }; -NBL_MAIN_FUNC(PathTracingApp) \ No newline at end of file +NBL_MAIN_FUNC(PathTracingApp) diff --git a/CMakeLists.txt b/CMakeLists.txt index a16bae025..74cb758b9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,8 +5,16 @@ if(NBL_BUILD_EXAMPLES) project(NablaExamples) enable_testing() + set(NBL_EXAMPLES_OPTIONAL_COMMON_TARGETS) if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.git") - NBL_ADD_GIT_TRACKING_META_LIBRARY(examples "${CMAKE_CURRENT_SOURCE_DIR}") + NBL_ADD_GIT_TRACKING_META_LIBRARY( + TARGET NblExamplesGitInfo + NAMESPACE nbl::examples::gtml + HEADER_PATH nbl/examples/git/info.h + REPOS + examples "${CMAKE_CURRENT_SOURCE_DIR}" + ) + list(APPEND NBL_EXAMPLES_OPTIONAL_COMMON_TARGETS NblExamplesGitInfo) endif() if(NBL_BUILD_ANDROID) @@ -119,7 +127,7 @@ if(NBL_BUILD_EXAMPLES) # add new examples *before* NBL_GET_ALL_TARGETS invocation, it gathers recursively all targets created so far in this subdirectory NBL_GET_ALL_TARGETS(TARGETS) - list(REMOVE_ITEM TARGETS examples_check_git) + list(REMOVE_ITEM TARGETS NblExamplesGitInfo) # we want to loop only over the examples so we exclude examples' interface libraries created in common subdirectory list(REMOVE_ITEM TARGETS ${NBL_EXAMPLES_API_TARGET} ${NBL_EXAMPLES_API_LIBRARIES}) @@ -127,8 +135,11 @@ if(NBL_BUILD_EXAMPLES) # we link common example api library and force examples to reuse its PCH foreach(T IN LISTS TARGETS) get_target_property(TYPE ${T} TYPE) - if(NOT ${TYPE} MATCHES INTERFACE) + if(NOT ${TYPE} MATCHES "INTERFACE|UTILITY") target_link_libraries(${T} PUBLIC ${NBL_EXAMPLES_API_TARGET}) + if(NBL_EXAMPLES_OPTIONAL_COMMON_TARGETS) + target_link_libraries(${T} PUBLIC ${NBL_EXAMPLES_OPTIONAL_COMMON_TARGETS}) + endif() target_include_directories(${T} PUBLIC $) set_target_properties(${T} PROPERTIES DISABLE_PRECOMPILE_HEADERS OFF) target_precompile_headers(${T} REUSE_FROM "${NBL_EXAMPLES_API_TARGET}") From a41b88c0d14536c299680290250880770156ffd0 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Wed, 11 Mar 2026 18:24:03 +0100 Subject: [PATCH 33/34] Add mesh loaders perf reference update flow --- 12_MeshLoaders/App.hpp | 2 +- 12_MeshLoaders/AppLifecycle.cpp | 51 ++++++++++++++++++++++++------- 12_MeshLoaders/AppPerf.cpp | 53 ++++++++++++++++++++++++++++++--- 12_MeshLoaders/CMakeLists.txt | 4 +-- 12_MeshLoaders/README.md | 10 +++++++ 5 files changed, 103 insertions(+), 17 deletions(-) diff --git a/12_MeshLoaders/App.hpp b/12_MeshLoaders/App.hpp index a78991380..a8a3ad64e 100644 --- a/12_MeshLoaders/App.hpp +++ b/12_MeshLoaders/App.hpp @@ -189,6 +189,7 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built std::optional referenceDir; std::optional profileOverride; bool strict = false; + bool updateReference = false; }; struct WrittenAssetRequest @@ -456,7 +457,6 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built smart_refctd_ptr m_loaderPerfLogger; asset::SFileIOPolicy::SRuntimeTuning::Mode m_runtimeTuningMode = asset::SFileIOPolicy::SRuntimeTuning::Mode::Heuristic; bool m_forceLoaderContentHashes = true; - bool m_updateGeometryHashReferences = false; std::optional m_referenceCamera; }; diff --git a/12_MeshLoaders/AppLifecycle.cpp b/12_MeshLoaders/AppLifecycle.cpp index 796ce5102..c26d19452 100644 --- a/12_MeshLoaders/AppLifecycle.cpp +++ b/12_MeshLoaders/AppLifecycle.cpp @@ -63,12 +63,12 @@ void setupMeshLoadersArgumentParser(argparse::ArgumentParser& parser) parser.add_argument("--perf-profile-override") .nargs(1) .help("Override the automatically generated performance profile id."); + parser.add_argument("--perf-update-reference") + .help("Write the current structured performance run to the matching reference path.") + .flag(); parser.add_argument("--loader-content-hashes") .help("Keep loader content hashes enabled. This is already the default for this example.") .flag(); - parser.add_argument("--update-references") - .help("Accept official benchmark/reference CLI without changing current local flow.") - .flag(); parser.add_argument("--runtime-tuning") .nargs(1) .help("Runtime tuning mode for loaders: sequential|heuristic|hybrid. Default: heuristic."); @@ -113,7 +113,6 @@ struct ParsedCommandLineOptions bool ci = false; bool forceRowViewForCurrentTestList = false; bool forceLoaderContentHashes = true; - bool updateGeometryHashReferences = false; system::path saveGeomPrefixPath; system::path screenshotPrefixPath; system::path testListPath; @@ -123,6 +122,7 @@ struct ParsedCommandLineOptions std::optional perfReferenceDir; std::optional perfProfileOverride; bool perfStrict = false; + bool perfUpdateReference = false; std::optional rowAddPath; uint32_t rowDuplicateCount = 0u; asset::SFileIOPolicy::SRuntimeTuning::Mode runtimeTuningMode = asset::SFileIOPolicy::SRuntimeTuning::Mode::Heuristic; @@ -143,6 +143,27 @@ system::path resolveRuntimeCWD(const system::path& preferred) return preferred; } +system::path makeShortRuntimePath(const system::path& inputPath) +{ + if (inputPath.empty()) + return inputPath; + + std::filesystem::path pathValue(inputPath.string()); + pathValue = pathValue.lexically_normal(); + if (!pathValue.is_absolute()) + return system::path(pathValue.generic_string()); + + std::error_code ec; + const auto cwd = std::filesystem::current_path(ec); + if (!ec) + { + const auto relativePath = std::filesystem::relative(pathValue, cwd, ec); + if (!ec && !relativePath.empty() && !relativePath.is_absolute() && relativePath.generic_string().size() < pathValue.generic_string().size()) + return system::path(relativePath.lexically_normal().generic_string()); + } + return system::path(pathValue.generic_string()); +} + system::path resolveDefaultTestListPath(const system::path& effectiveInputCWD, const core::vector& argv) { const auto tryExisting = [](std::filesystem::path candidate) -> std::optional @@ -395,7 +416,7 @@ bool parseMeshLoadersCommandLine( } if (tmp.is_relative()) tmp = effectiveOutputCWD / tmp; - out.perfDumpDir = tmp; + out.perfDumpDir = makeShortRuntimePath(tmp); } if (parser.present("--perf-ref-dir")) { @@ -407,7 +428,7 @@ bool parseMeshLoadersCommandLine( } if (tmp.is_relative()) tmp = effectiveOutputCWD / tmp; - out.perfReferenceDir = tmp; + out.perfReferenceDir = makeShortRuntimePath(tmp); } if (parser["--perf-strict"] == true) out.perfStrict = true; @@ -421,10 +442,10 @@ bool parseMeshLoadersCommandLine( } out.perfProfileOverride = value; } + if (parser["--perf-update-reference"] == true) + out.perfUpdateReference = true; if (parser["--loader-content-hashes"] == true) out.forceLoaderContentHashes = true; - if (parser["--update-references"] == true) - out.updateGeometryHashReferences = true; if (parser.present("--runtime-tuning")) { if (!parseRuntimeTuningMode(parser.get("--runtime-tuning"), out.runtimeTuningMode)) @@ -433,6 +454,16 @@ bool parseMeshLoadersCommandLine( return false; } } + if (out.perfStrict && out.perfUpdateReference) + { + error = "Use either --perf-strict or --perf-update-reference, not both."; + return false; + } + if (out.perfUpdateReference && !out.perfReferenceDir.has_value()) + { + error = "--perf-update-reference requires --perf-ref-dir."; + return false; + } return true; } @@ -487,9 +518,9 @@ bool MeshLoadersApp::onAppInitialized(smart_refctd_ptr&& system) m_perf.options.referenceDir = std::move(options.perfReferenceDir); m_perf.options.profileOverride = std::move(options.perfProfileOverride); m_perf.options.strict = options.perfStrict; - m_perf.enabled = m_perf.options.dumpDir.has_value() || m_perf.options.referenceDir.has_value() || m_perf.options.strict; + m_perf.options.updateReference = options.perfUpdateReference; + m_perf.enabled = m_perf.options.dumpDir.has_value() || m_perf.options.referenceDir.has_value() || m_perf.options.strict || m_perf.options.updateReference; m_forceLoaderContentHashes = options.forceLoaderContentHashes; - m_updateGeometryHashReferences = options.updateGeometryHashReferences; m_runtimeTuningMode = options.runtimeTuningMode; }; applyParsedCommandLineOptions(std::move(parsed)); diff --git a/12_MeshLoaders/AppPerf.cpp b/12_MeshLoaders/AppPerf.cpp index e91ef2417..b7531acfb 100644 --- a/12_MeshLoaders/AppPerf.cpp +++ b/12_MeshLoaders/AppPerf.cpp @@ -277,6 +277,43 @@ perf_json_t buildCaseJson(const MeshLoadersApp::CasePerformanceMetrics& metrics, }; } +bool writePerfJson(system::ISystem* const system, const system::path& path, const perf_json_t& json) +{ + if (!system) + return false; + + const auto parentDir = path.parent_path(); + if (!parentDir.empty()) + std::filesystem::create_directories(parentDir); + if (std::filesystem::exists(path)) + { + std::error_code ec; + std::filesystem::remove(path, ec); + if (ec && !system->deleteFile(path)) + return false; + } + + system::ISystem::future_t> writeFileFuture; + system->createFile(writeFileFuture, path, system::IFile::ECF_WRITE); + core::smart_refctd_ptr writeFile; + writeFileFuture.acquire().move_into(writeFile); + if (!writeFile) + return false; + + const auto serialized = json.dump(2); + size_t written = 0ull; + while (written < serialized.size()) + { + system::IFile::success_t success; + writeFile->write(success, serialized.data() + written, written, serialized.size() - written); + const auto processed = success.getBytesProcessed(); + if (!success || processed == 0ull) + return false; + written += processed; + } + return true; +} + } bool MeshLoadersApp::performanceEnabled() const @@ -442,7 +479,7 @@ void MeshLoadersApp::finalizePerformanceRun() { m_perf.referencePath = *m_perf.options.referenceDir / m_perf.workloadId / (m_perf.profileId + ".json"); root["reference"]["lookup_key"] = m_perf.workloadId + "/" + m_perf.profileId + ".json"; - if (std::filesystem::exists(m_perf.referencePath)) + if (!m_perf.options.updateReference && std::filesystem::exists(m_perf.referencePath)) { m_perf.referenceMatched = true; std::ifstream stream(m_perf.referencePath); @@ -483,6 +520,7 @@ void MeshLoadersApp::finalizePerformanceRun() } root["reference"]["matched"] = m_perf.referenceMatched; root["reference"]["strict"] = m_perf.options.strict; + root["reference"]["updated"] = m_perf.options.updateReference; root["reference"]["comparison_failures"] = m_perf.comparisonFailures; if (m_perf.options.dumpDir) @@ -490,13 +528,20 @@ void MeshLoadersApp::finalizePerformanceRun() const auto dumpDir = *m_perf.options.dumpDir / m_perf.workloadId; std::filesystem::create_directories(dumpDir); m_perf.dumpPath = dumpDir / (currentTimestampTag() + "__" + m_perf.profileId + ".json"); - std::ofstream out(m_perf.dumpPath); - out << root.dump(2); + if (!writePerfJson(m_system.get(), m_perf.dumpPath, root)) + failExit("Failed to write performance dump file: %s", m_perf.dumpPath.string().c_str()); + } + if (m_perf.options.updateReference) + { + if (!writePerfJson(m_system.get(), m_perf.referencePath, root)) + failExit("Failed to write performance reference file: %s", m_perf.referencePath.string().c_str()); } if (m_logger) { - if (!m_perf.referenceMatched) + if (m_perf.options.updateReference) + m_logger->log("Performance reference updated for workload=%s profile=%s.", ILogger::ELL_INFO, m_perf.workloadId.c_str(), m_perf.profileId.c_str()); + else if (!m_perf.referenceMatched) m_logger->log("Performance reference not found for workload=%s profile=%s.", ILogger::ELL_INFO, m_perf.workloadId.c_str(), m_perf.profileId.c_str()); else if (m_perf.comparisonFailures.empty()) m_logger->log("Performance reference comparison passed for workload=%s profile=%s.", ILogger::ELL_INFO, m_perf.workloadId.c_str(), m_perf.profileId.c_str()); diff --git a/12_MeshLoaders/CMakeLists.txt b/12_MeshLoaders/CMakeLists.txt index c6f6f3953..c92f6a034 100644 --- a/12_MeshLoaders/CMakeLists.txt +++ b/12_MeshLoaders/CMakeLists.txt @@ -111,8 +111,8 @@ if (NBL_MESHLOADERS_ENABLE_BENCHMARK_DATASETS) --ci --testlist "${NBL_MESHLOADERS_BENCHMARK_INPUTS_JSON}" --loader-content-hashes - --perf-dump-dir "${NBL_MESHLOADERS_PERF_DUMP_DIR}" - --perf-ref-dir "${NBL_MESHLOADERS_PERF_REFERENCE_DIR}" + --perf-dump-dir "../../../build/dynamic/meshloaders_perf_runs" + --perf-ref-dir "../../../build/dynamic/meshloaders_benchmark_datasets/meshloaders/perf_refs" --perf-strict ) diff --git a/12_MeshLoaders/README.md b/12_MeshLoaders/README.md index a7dc56362..364091a44 100644 --- a/12_MeshLoaders/README.md +++ b/12_MeshLoaders/README.md @@ -89,13 +89,20 @@ Example for loading and writing `OBJ`, `PLY` and `STL` meshes. - IO runtime tuning mode - `--perf-dump-dir ` - write structured performance run JSON artifacts + - relative paths resolve against local output CWD - `--perf-ref-dir ` - lookup directory for structured performance references + - relative paths resolve against local output CWD + - absolute paths may be shortened internally when a shorter relative runtime path is available - `--perf-strict` - fail on performance regression only when a matching reference exists - if no matching reference exists, the run stays record-only - `--perf-profile-override ` - override the automatically derived machine profile id +- `--perf-update-reference` + - write the current structured performance run to the matching reference path + - requires `--perf-ref-dir` + - cannot be combined with `--perf-strict` ## Controls (non-CI) - Arrow keys: move camera @@ -150,6 +157,9 @@ Rules: - If a matching reference exists: - per-case `original_load`, `write`, and `written_load` stage metrics are compared - strict mode fails only on actual regression, not on missing references +- If `--perf-update-reference` is used: + - the current run is written directly to `//.json` + - existing comparison is skipped for that run - JSON artifacts avoid host-specific absolute paths and store portable case/test-list identifiers instead ## Performance logs to trust From 187aebeaf3287ba5ba3a6e872bd682af988d8e85 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Wed, 11 Mar 2026 20:05:12 +0100 Subject: [PATCH 34/34] Stabilize mesh loaders perf metrics --- 12_MeshLoaders/App.hpp | 2 +- 12_MeshLoaders/AppLifecycle.cpp | 4 ++-- 12_MeshLoaders/AppPerf.cpp | 8 +------- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/12_MeshLoaders/App.hpp b/12_MeshLoaders/App.hpp index a8a3ad64e..c3a0acc8b 100644 --- a/12_MeshLoaders/App.hpp +++ b/12_MeshLoaders/App.hpp @@ -332,7 +332,7 @@ class MeshLoadersApp final : public MeshLoadersWindowedApplication, public Built protected: core::bitflag getLogLevelMask() override { - return system::ILogger::DefaultLogMask() | system::ILogger::ELL_INFO; + return core::bitflag(system::ILogger::ELL_WARNING) | system::ILogger::ELL_ERROR; } const video::IGPURenderpass::SCreationParams::SSubpassDependency* getDefaultSubpassDependencies() const override; diff --git a/12_MeshLoaders/AppLifecycle.cpp b/12_MeshLoaders/AppLifecycle.cpp index c26d19452..b88450605 100644 --- a/12_MeshLoaders/AppLifecycle.cpp +++ b/12_MeshLoaders/AppLifecycle.cpp @@ -1065,7 +1065,7 @@ bool MeshLoadersApp::startCase(const size_t index) bool loaded = false; LoadStageMetrics loadMetrics = {}; - if (m_runtime.mode == RunMode::CI) + if (m_runtime.mode == RunMode::CI && !performanceEnabled()) { PreparedAssetLoad preparedLoad = {}; bool preparedReady = false; @@ -1094,7 +1094,7 @@ bool MeshLoadersApp::startCase(const size_t index) if (m_runtime.mode == RunMode::CI) { const auto nextIndex = index + 1u; - if (nextIndex < m_runtime.cases.size()) + if (!performanceEnabled() && nextIndex < m_runtime.cases.size()) startPreparedAssetLoad(nextIndex, m_runtime.cases[nextIndex].path); } diff --git a/12_MeshLoaders/AppPerf.cpp b/12_MeshLoaders/AppPerf.cpp index b7531acfb..de683aa7c 100644 --- a/12_MeshLoaders/AppPerf.cpp +++ b/12_MeshLoaders/AppPerf.cpp @@ -285,13 +285,7 @@ bool writePerfJson(system::ISystem* const system, const system::path& path, cons const auto parentDir = path.parent_path(); if (!parentDir.empty()) std::filesystem::create_directories(parentDir); - if (std::filesystem::exists(path)) - { - std::error_code ec; - std::filesystem::remove(path, ec); - if (ec && !system->deleteFile(path)) - return false; - } + system->deleteFile(path); system::ISystem::future_t> writeFileFuture; system->createFile(writeFileFuture, path, system::IFile::ECF_WRITE);