From 2c3d2afbf44ad37113baabe2423889ace9a17ebb Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Thu, 5 Feb 2026 19:52:26 +0100 Subject: [PATCH 01/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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());