app/cli.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // SPDX-License-Identifier: BSD-3-Clause | ||
| 2 | #include "chrono_batch_render/app/cli.h" | ||
| 3 | |||
| 4 | #include <algorithm> | ||
| 5 | #include <charconv> | ||
| 6 | #include <chrono> | ||
| 7 | #include <cstdio> | ||
| 8 | #include <cstdlib> | ||
| 9 | #include <deque> | ||
| 10 | #include <exception> | ||
| 11 | #include <filesystem> | ||
| 12 | #include <optional> | ||
| 13 | #include <string_view> | ||
| 14 | |||
| 15 | #include <fmt/format.h> | ||
| 16 | |||
| 17 | #if defined(_WIN32) | ||
| 18 | #include <windows.h> | ||
| 19 | #else | ||
| 20 | #include <dlfcn.h> | ||
| 21 | #endif | ||
| 22 | |||
| 23 | #ifndef CHRONO_BATCH_RENDER_VIEWER_FILE | ||
| 24 | #define CHRONO_BATCH_RENDER_VIEWER_FILE "chrono_batch_render_viewer.dll" | ||
| 25 | #endif | ||
| 26 | #ifndef CHRONO_BATCH_RENDER_XR_FILE | ||
| 27 | #define CHRONO_BATCH_RENDER_XR_FILE "chrono_batch_render_xr.dll" | ||
| 28 | #endif | ||
| 29 | |||
| 30 | #include "chrono_batch_render/chrono_batch_render.h" | ||
| 31 | |||
| 32 | #ifdef CHRONO_BATCH_RENDER_APP_XR | ||
| 33 | #include "chrono_batch_render/xr/run.h" | ||
| 34 | #endif | ||
| 35 | |||
| 36 | namespace chrono::batch_render::app { | ||
| 37 | namespace { | ||
| 38 | |||
| 39 | constexpr const char* kHelp = R"(Usage: chrono-batch-render [options] | ||
| 40 | |||
| 41 | Opens the viewer on the default scene, if this Chrono Batch Render has one, or renders it without a window. | ||
| 42 | |||
| 43 | Options | ||
| 44 | -h, --help Show this help | ||
| 45 | -V, --version Show the build's version and commit | ||
| 46 | -v, --verbose Log debug messages too | ||
| 47 | -q, --quiet Log only warnings and errors | ||
| 48 | --devices List the adapters and what each supports | ||
| 49 | --device ALIAS Render on an adapter from --devices, such as vulkan:0 or cuda:0 | ||
| 50 | --validation Turn on the backend's validation | ||
| 51 | --traversal MODE Ray traversal, auto, hardware or software | ||
| 52 | --sampling MODE Texture sampling, auto, hardware or software | ||
| 53 | |||
| 54 | Scene | ||
| 55 | --worlds N Clone the default scene into N worlds, each with its own camera | ||
| 56 | --camera PATH Render from this camera instead of each world's /World/Camera | ||
| 57 | --product PATH Render this render product as authored instead of Chrono Batch Render's own | ||
| 58 | --preset NAME Shade with real-time, the default, or minimal | ||
| 59 | --size WxH The image or window size, such as 1600x900 | ||
| 60 | --aovs A,B Also render these AOVs, such as DistanceToCamera,Normal | ||
| 61 | |||
| 62 | Without a window | ||
| 63 | --headless Render without opening the viewer | ||
| 64 | --frames N Steps to render without a window, 1 by default, or to time, 100 | ||
| 65 | --output DIR Write the last frame's AOVs to DIR, which implies --headless | ||
| 66 | --benchmark Time steps one at a time and three in flight | ||
| 67 | --warmup N Steps a benchmark takes before timing, 10 by default | ||
| 68 | --json Print a benchmark's timings as one JSON line | ||
| 69 | --profile Print the profiler's report of the timed steps | ||
| 70 | |||
| 71 | Viewer | ||
| 72 | --vsync Wait for the display's refresh when presenting | ||
| 73 | --aov NAME The AOV shown, such as LdrColor, Normal or DistanceToCamera | ||
| 74 | --layout FILE A viewer layout to open with, saved from Window > Save Layout | ||
| 75 | )" | ||
| 76 | #ifdef CHRONO_BATCH_RENDER_APP_XR | ||
| 77 | R"( | ||
| 78 | XR | ||
| 79 | --xr Start XR on the headset through the OpenXR runtime, in the viewer's | ||
| 80 | XR panel or, with --headless, without a window until Ctrl+C | ||
| 81 | --xr-sim Start XR on a simulated headset instead | ||
| 82 | --xr-record FILE Save the headset's input to FILE when XR stops | ||
| 83 | --xr-replay FILE Play input saved with --xr-record back through the simulated | ||
| 84 | headset, which implies --xr-sim | ||
| 85 | )" | ||
| 86 | #endif | ||
| 87 | ; | ||
| 88 | |||
| 89 | 16 | uint32_t ParseNumber(std::string_view text, std::string_view flag) { | |
| 90 | 16 | uint32_t number = 0; | |
| 91 | 16 | const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), number); | |
| 92 | 16 | if (error != std::errc() || end != text.data() + text.size() || number == 0) { | |
| 93 | 2 | throw Error("{} takes a number above 0, not \"{}\"", flag, text); | |
| 94 | } | ||
| 95 | 14 | return number; | |
| 96 | } | ||
| 97 | |||
| 98 | 3 | template <class Mode> Mode ParseMode(std::string_view text, std::string_view flag) { | |
| 99 | 3 | if (text == "auto") { | |
| 100 | return Mode::Auto; | ||
| 101 | } | ||
| 102 | 3 | if (text == "hardware") { | |
| 103 | return Mode::Hardware; | ||
| 104 | } | ||
| 105 | 2 | if (text == "software") { | |
| 106 | return Mode::Software; | ||
| 107 | } | ||
| 108 | 1 | throw Error("{} takes auto, hardware or software, not \"{}\"", flag, text); | |
| 109 | } | ||
| 110 | |||
| 111 | 1 | void PrintDevices() { | |
| 112 | 2 | for (const device::AdapterInfo& adapter : device::EnumerateAdapters()) { | |
| 113 | 2 | std::printf( | |
| 114 | "%-10s %s, %.1f GiB%s%s%s\n", adapter.alias.c_str(), adapter.name.c_str(), | ||
| 115 | 1 | double(adapter.memoryBytes) / double(1ull << 30), | |
| 116 | 1 | adapter.features.presentation ? ", presents" : "", | |
| 117 | 1 | adapter.unsupported.empty() ? "" : ", unsupported: ", adapter.unsupported.c_str()); | |
| 118 | 1 | } | |
| 119 | 1 | } | |
| 120 | |||
| 121 | constexpr double kStep = 1.0 / 60; | ||
| 122 | |||
| 123 | 4 | std::string WorldPath(uint32_t world) { | |
| 124 | 4 | return world == 0 ? std::string("/World") : fmt::format("/World_{}", world); | |
| 125 | } | ||
| 126 | |||
| 127 | ✗ | void PrintProfile() { | |
| 128 | ✗ | const profiler::Report report = profiler::Summarize(profiler::Frames()); | |
| 129 | ✗ | std::fputs(profiler::ReportText(report).c_str(), stderr); | |
| 130 | ✗ | } | |
| 131 | |||
| 132 | 5 | Ref<Scene> DefaultScene(device::Device& device, uint32_t worlds, bool waitForKernels) { | |
| 133 | ✗ | const Ref<Scene> scene = Scene::Create(device, {.waitForKernels = waitForKernels}); | |
| 134 | 5 | if (worlds > 1) { | |
| 135 | 1 | scene->Define<World>("/World"); | |
| 136 | } else { | ||
| 137 | 4 | scene->Define<Xform>("/World"); | |
| 138 | } | ||
| 139 | 5 | scene->Define<Scope>("/World/Looks"); | |
| 140 | 5 | const std::pair<const char*, Float3> looks[] = {{"Red", {0.8f, 0.3f, 0.2f}}, | |
| 141 | {"Blue", {0.2f, 0.5f, 0.8f}}, | ||
| 142 | {"Green", {0.3f, 0.7f, 0.3f}}, | ||
| 143 | {"Yellow", {0.9f, 0.7f, 0.2f}}, | ||
| 144 | {"Purple", {0.6f, 0.4f, 0.8f}}}; | ||
| 145 | 30 | for (const auto& [name, color] : looks) { | |
| 146 | 25 | const std::string look = fmt::format("/World/Looks/{}", name); | |
| 147 | 25 | const Prim material = scene->Define<Material>(look); | |
| 148 | 25 | const Prim surface = scene->Define<Shader>(look + "/Surface", {.id = "UsdPreviewSurface"}); | |
| 149 | 25 | surface.Set(UsdPreviewSurface::kDiffuseColor, color); | |
| 150 | 25 | material.Connect("surface", surface, "surface"); | |
| 151 | 25 | } | |
| 152 | 5 | scene->Define<Plane>("/World/Ground", {.checkerboard = true, | |
| 153 | .checkerColorA = {0.6f, 0.6f, 0.6f}, | ||
| 154 | .checkerColorB = {0.3f, 0.3f, 0.3f}, | ||
| 155 | .width = 0, | ||
| 156 | .length = 0}); | ||
| 157 | 5 | scene->Define<Cube>("/World/Cube", | |
| 158 | {.materialBinding = {"/World/Looks/Red"}, .translate = {-6, 0, 1}}); | ||
| 159 | 5 | scene->Define<Sphere>("/World/Sphere", | |
| 160 | {.materialBinding = {"/World/Looks/Blue"}, .translate = {-3, 0, 1}}); | ||
| 161 | 5 | scene->Define<Cylinder>("/World/Cylinder", | |
| 162 | {.materialBinding = {"/World/Looks/Green"}, .translate = {0, 0, 1}}); | ||
| 163 | 5 | scene->Define<Cone>("/World/Cone", | |
| 164 | {.materialBinding = {"/World/Looks/Yellow"}, .translate = {3, 0, 1}}); | ||
| 165 | 5 | scene->Define<Capsule>("/World/Capsule", | |
| 166 | {.materialBinding = {"/World/Looks/Purple"}, .translate = {6, 0, 1}}); | ||
| 167 | 5 | scene->Define<DistantLight>("/World/Sun", {.orient = FromAzimuthElevation(146, 44)}); | |
| 168 | |||
| 169 | 5 | const Double3 eye{0, -15.6, 3.4}; | |
| 170 | 10 | scene->Define<Camera>("/World/Camera", {.translate = eye, | |
| 171 | 5 | .orient = LookAt(eye, {0, 0, 1}), | |
| 172 | .focalLength = 29, | ||
| 173 | .horizontalAperture = 24.0f * 16 / 9, | ||
| 174 | .verticalAperture = 24}); | ||
| 175 | |||
| 176 | 5 | scene->Clone(*scene->Find("/World"), worlds - 1); | |
| 177 | 5 | return scene; | |
| 178 | 135 | } | |
| 179 | |||
| 180 | 5 | std::string ChooseProduct(Scene& scene, const Command& command) { | |
| 181 | 5 | if (!command.product.empty()) { | |
| 182 | 1 | const std::optional<Prim> product = scene.Find(command.product); | |
| 183 | 1 | if (!product || !product->IsA(SchemaId::RenderProduct)) { | |
| 184 | 1 | throw Error("The scene has no render product {}", command.product); | |
| 185 | } | ||
| 186 | ✗ | return command.product; | |
| 187 | } | ||
| 188 | 4 | const bool viewer = command.kind == Command::Kind::Viewer; | |
| 189 | 4 | if (viewer && command.camera.empty() && command.preset.empty() && command.aovs.empty()) { | |
| 190 | ✗ | return {}; | |
| 191 | } | ||
| 192 | |||
| 193 | 4 | std::vector<std::string> cameras; | |
| 194 | 4 | if (!command.camera.empty()) { | |
| 195 | 1 | const std::optional<Prim> camera = scene.Find(command.camera); | |
| 196 | 1 | if (!camera || !camera->IsA(SchemaId::Camera)) { | |
| 197 | 1 | throw Error("The scene has no camera {}", command.camera); | |
| 198 | } | ||
| 199 | ✗ | cameras.push_back(command.camera); | |
| 200 | } else { | ||
| 201 | 7 | for (uint32_t world = 0; world < command.worlds; ++world) { | |
| 202 | 8 | cameras.push_back(WorldPath(world) + "/Camera"); | |
| 203 | } | ||
| 204 | } | ||
| 205 | |||
| 206 | 3 | scene.Define<Scope>("/Render"); | |
| 207 | 3 | std::vector<std::string> vars; | |
| 208 | 3 | std::vector<std::string> names = {"LdrColor"}; | |
| 209 | 3 | names.insert(names.end(), command.aovs.begin(), command.aovs.end()); | |
| 210 | 7 | for (const std::string& name : names) { | |
| 211 | 5 | const std::string path = "/Render/" + name; | |
| 212 | 5 | if (std::ranges::find(vars, path) == vars.end()) { | |
| 213 | 5 | if (!FindRenderVarSource(name)) { | |
| 214 | 1 | throw Error("\"{}\" is no AOV; try LdrColor, HdrColor, DistanceToCamera or Normal", | |
| 215 | 1 | name); | |
| 216 | } | ||
| 217 | 5 | scene.Define<RenderVar>(path, {.sourceName = name}); | |
| 218 | 4 | vars.push_back(path); | |
| 219 | } | ||
| 220 | 5 | } | |
| 221 | 2 | const std::string product = "/Render/Product"; | |
| 222 | 2 | scene.Define<RenderProduct>( | |
| 223 | product, | ||
| 224 | 2 | {.renderMode = command.preset == "minimal" ? RenderMode::Minimal : RenderMode::RealTime, | |
| 225 | .camera = cameras, | ||
| 226 | 2 | .resolution = {command.width, command.height}, | |
| 227 | .orderedVars = vars}); | ||
| 228 | 2 | return product; | |
| 229 | 17 | } | |
| 230 | |||
| 231 | 1 | int RenderFrames(const Ref<Scene>& scene, const std::string& product, const Command& command) { | |
| 232 | 2 | Renderer renderer(scene); | |
| 233 | 1 | profiler::SetEnabled(command.profile); | |
| 234 | 1 | const uint32_t frames = std::max(command.frames, 1u); | |
| 235 | 1 | RenderOutput output; | |
| 236 | 2 | for (uint32_t frame = 0; frame < frames; ++frame) { | |
| 237 | 1 | profiler::BeginFrame(); | |
| 238 | 1 | scene->Advance(kStep); | |
| 239 | 1 | output = renderer.Step(product); | |
| 240 | } | ||
| 241 | 1 | output.Wait(); | |
| 242 | 1 | if (!command.output.empty()) { | |
| 243 | 2 | for (const std::filesystem::path& written : | |
| 244 | 5 | WriteFrame(command.output, output[product].frames.back())) { | |
| 245 | 8 | Log(LogLevel::Info, "wrote {}", written.string()); | |
| 246 | 1 | } | |
| 247 | } | ||
| 248 | 1 | if (command.profile) { | |
| 249 | ✗ | PrintProfile(); | |
| 250 | } | ||
| 251 | 2 | return 0; | |
| 252 | 1 | } | |
| 253 | |||
| 254 | 1 | int Benchmark(const Ref<Scene>& scene, const std::string& product, const Command& command) { | |
| 255 | 1 | using Clock = std::chrono::steady_clock; | |
| 256 | 2 | Renderer renderer(scene); | |
| 257 | 1 | const uint32_t steps = command.frames > 0 ? command.frames : 100; | |
| 258 | 1 | int64_t pixels = 0; | |
| 259 | 2 | for (uint32_t step = 0; step < std::max(command.warmup, 1u); ++step) { | |
| 260 | 1 | scene->Advance(kStep); | |
| 261 | 1 | const RenderOutput output = renderer.Step(product); | |
| 262 | 1 | output.Wait(); | |
| 263 | 1 | const ProductOutput& rendered = output[product]; | |
| 264 | 1 | if (!rendered.frames.empty() && !rendered.frames.front().renderVars.empty()) { | |
| 265 | 1 | const Span<const int64_t> shape = | |
| 266 | 1 | rendered.frames.front().renderVars.begin()->second.tensor.Shape(); | |
| 267 | 1 | pixels = shape.size() >= 3 ? shape[0] * shape[1] * shape[2] : 0; | |
| 268 | } | ||
| 269 | 1 | } | |
| 270 | |||
| 271 | 1 | profiler::SetEnabled(command.profile); | |
| 272 | 1 | const Clock::time_point serialStart = Clock::now(); | |
| 273 | 3 | for (uint32_t step = 0; step < steps; ++step) { | |
| 274 | 2 | profiler::BeginFrame(); | |
| 275 | 2 | scene->Advance(kStep); | |
| 276 | 4 | renderer.Step(product).Wait(); | |
| 277 | } | ||
| 278 | 1 | const double latencyMs = | |
| 279 | 1 | std::chrono::duration<double, std::milli>(Clock::now() - serialStart).count() / steps; | |
| 280 | |||
| 281 | 1 | std::deque<RenderOutput> inFlight; | |
| 282 | 1 | const Clock::time_point pipelinedStart = Clock::now(); | |
| 283 | 3 | for (uint32_t step = 0; step < steps; ++step) { | |
| 284 | 2 | profiler::BeginFrame(); | |
| 285 | 2 | scene->Advance(kStep); | |
| 286 | 4 | inFlight.push_back(renderer.Step(product)); | |
| 287 | 2 | if (inFlight.size() == 3) { | |
| 288 | ✗ | inFlight.front().Wait(); | |
| 289 | ✗ | inFlight.pop_front(); | |
| 290 | } | ||
| 291 | } | ||
| 292 | 3 | for (const RenderOutput& output : inFlight) { | |
| 293 | 4 | output.Wait(); | |
| 294 | } | ||
| 295 | 1 | const double seconds = std::chrono::duration<double>(Clock::now() - pipelinedStart).count(); | |
| 296 | 1 | const double fps = steps / seconds; | |
| 297 | 1 | const double megapixels = double(pixels) * fps / 1e6; | |
| 298 | |||
| 299 | 1 | if (command.json) { | |
| 300 | 2 | std::printf("{\"product\": \"%s\", \"worlds\": %u, \"pixels\": %lld, \"warmup\": %u, " | |
| 301 | "\"steps\": %u, \"latency_ms\": %.4f, \"fps\": %.2f, " | ||
| 302 | "\"megapixels_per_second\": %.2f}\n", | ||
| 303 | 1 | product.c_str(), command.worlds, static_cast<long long>(pixels), command.warmup, | |
| 304 | steps, latencyMs, fps, megapixels); | ||
| 305 | } else { | ||
| 306 | ✗ | std::printf("%s, %lld pixels a step\n", product.c_str(), static_cast<long long>(pixels)); | |
| 307 | ✗ | std::printf(" one step at a time %.3f ms\n", latencyMs); | |
| 308 | ✗ | std::printf(" three in flight %.1f steps a second, %.1f megapixels a second\n", fps, | |
| 309 | megapixels); | ||
| 310 | } | ||
| 311 | 1 | std::fflush(stdout); | |
| 312 | 1 | if (command.profile) { | |
| 313 | ✗ | PrintProfile(); | |
| 314 | } | ||
| 315 | 1 | return 0; | |
| 316 | 1 | } | |
| 317 | |||
| 318 | ✗ | void* FindEntry([[maybe_unused]] const char* file, [[maybe_unused]] const char* name, | |
| 319 | const char* symbol) { | ||
| 320 | #if defined(_WIN32) | ||
| 321 | HMODULE self = nullptr; | ||
| 322 | GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | | ||
| 323 | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, | ||
| 324 | reinterpret_cast<LPCWSTR>(&FindEntry), &self); | ||
| 325 | wchar_t path[MAX_PATH] = {}; | ||
| 326 | GetModuleFileNameW(self, path, MAX_PATH); | ||
| 327 | const std::filesystem::path library = std::filesystem::path(path).parent_path() / file; | ||
| 328 | const HMODULE module = | ||
| 329 | LoadLibraryExW(library.c_str(), nullptr, | ||
| 330 | LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); | ||
| 331 | return module ? reinterpret_cast<void*>(GetProcAddress(module, symbol)) : nullptr; | ||
| 332 | #else | ||
| 333 | ✗ | const std::string stem = std::string("lib") + name; | |
| 334 | #if defined(__APPLE__) | ||
| 335 | const std::string names[] = {"@rpath/" + stem + "." CHRONO_BATCH_RENDER_SOVERSION ".dylib", | ||
| 336 | "@rpath/" + stem + ".dylib"}; | ||
| 337 | #else | ||
| 338 | ✗ | const std::string names[] = {stem + ".so." CHRONO_BATCH_RENDER_SOVERSION, stem + ".so"}; | |
| 339 | #endif | ||
| 340 | ✗ | for (const std::string& candidate : names) { | |
| 341 | ✗ | if (void* module = dlopen(candidate.c_str(), RTLD_NOW | RTLD_LOCAL)) { | |
| 342 | ✗ | return dlsym(module, symbol); | |
| 343 | } | ||
| 344 | } | ||
| 345 | return nullptr; | ||
| 346 | #endif | ||
| 347 | ✗ | } | |
| 348 | |||
| 349 | ✗ | RunViewer FindViewer() { | |
| 350 | ✗ | return reinterpret_cast<RunViewer>( | |
| 351 | ✗ | FindEntry(CHRONO_BATCH_RENDER_VIEWER_FILE, "chrono_batch_render_viewer", kRunViewer)); | |
| 352 | } | ||
| 353 | |||
| 354 | #ifdef CHRONO_BATCH_RENDER_APP_XR | ||
| 355 | int RunXr(const Command& command) { | ||
| 356 | const auto run = reinterpret_cast<xr::RunXr>( | ||
| 357 | FindEntry(CHRONO_BATCH_RENDER_XR_FILE, "chrono_batch_render_xr", xr::kRunXr)); | ||
| 358 | if (run == nullptr) { | ||
| 359 | throw Error("This Chrono Batch Render has no XR. Install it with the " | ||
| 360 | "chrono-batch-render-viewer package or the chrono-batch-render wheel"); | ||
| 361 | } | ||
| 362 | const xr::RunDesc desc{.simulated = command.xr == XrMode::Simulated, | ||
| 363 | .device = command.device, | ||
| 364 | .camera = | ||
| 365 | command.camera.empty() ? WorldPath(0) + "/Camera" : command.camera, | ||
| 366 | .frames = command.frames, | ||
| 367 | .profile = command.profile, | ||
| 368 | .record = command.xrRecord, | ||
| 369 | .replay = command.xrReplay, | ||
| 370 | .scene = [&](device::Device& device) { | ||
| 371 | return DefaultScene(device, command.worlds, false); | ||
| 372 | }}; | ||
| 373 | std::string error; | ||
| 374 | const int status = run(&desc, &error); | ||
| 375 | if (!error.empty()) { | ||
| 376 | throw Error("{}", error); | ||
| 377 | } | ||
| 378 | return status; | ||
| 379 | } | ||
| 380 | #endif | ||
| 381 | |||
| 382 | } | ||
| 383 | |||
| 384 | 39 | Command ParseCommand(const std::vector<std::string>& args) { | |
| 385 | 39 | Command command; | |
| 386 | 39 | bool headless = false; | |
| 387 | 39 | bool benchmark = false; | |
| 388 | 39 | std::string benchmarkOnly; | |
| 389 | 96 | for (size_t at = 0; at < args.size(); ++at) { | |
| 390 | 68 | std::string_view flag = args[at]; | |
| 391 | 68 | std::optional<std::string_view> attached; | |
| 392 | 68 | if (const size_t equals = flag.find('='); flag.starts_with("--") && equals != flag.npos) { | |
| 393 | 2 | attached = flag.substr(equals + 1); | |
| 394 | 2 | flag = flag.substr(0, equals); | |
| 395 | } | ||
| 396 | 68 | bool taken = false; | |
| 397 | 104 | const auto value = [&]() -> std::string_view { | |
| 398 | 36 | taken = true; | |
| 399 | 36 | if (attached) { | |
| 400 | 1 | return *attached; | |
| 401 | } | ||
| 402 | 35 | if (++at == args.size()) { | |
| 403 | 1 | throw Error("{} needs a value", flag); | |
| 404 | } | ||
| 405 | 34 | return args[at]; | |
| 406 | 68 | }; | |
| 407 | 68 | if (flag == "-h" || flag == "--help") { | |
| 408 | 3 | command.kind = Command::Kind::Help; | |
| 409 | 65 | } else if (flag == "-v" || flag == "--verbose" || flag == "-q" || flag == "--quiet") { | |
| 410 | 7 | const LogLevel level = | |
| 411 | 7 | flag == "-v" || flag == "--verbose" ? LogLevel::Debug : LogLevel::Warning; | |
| 412 | 7 | if (command.logLevel && *command.logLevel != level) { | |
| 413 | 2 | throw Error("--verbose and --quiet can't be used together"); | |
| 414 | } | ||
| 415 | 6 | command.logLevel = level; | |
| 416 | 58 | } else if (flag == "-V" || flag == "--version") { | |
| 417 | 3 | command.kind = Command::Kind::Version; | |
| 418 | 55 | } else if (flag == "--devices") { | |
| 419 | 2 | command.kind = Command::Kind::Devices; | |
| 420 | 53 | } else if (flag == "--device") { | |
| 421 | 2 | command.device.adapter = value(); | |
| 422 | 51 | } else if (flag == "--validation") { | |
| 423 | 1 | command.device.validation = true; | |
| 424 | 50 | } else if (flag == "--traversal") { | |
| 425 | 2 | command.device.traversal = ParseMode<device::Traversal>(value(), flag); | |
| 426 | 48 | } else if (flag == "--sampling") { | |
| 427 | 1 | command.device.sampling = ParseMode<device::Sampling>(value(), flag); | |
| 428 | 47 | } else if (flag == "--size") { | |
| 429 | 6 | const std::string_view size = value(); | |
| 430 | 6 | const size_t x = size.find('x'); | |
| 431 | 6 | if (x == size.npos) { | |
| 432 | 2 | throw Error("--size takes WxH, such as 1600x900, not \"{}\"", size); | |
| 433 | } | ||
| 434 | 4 | command.width = ParseNumber(size.substr(0, x), flag); | |
| 435 | 3 | command.height = ParseNumber(size.substr(x + 1), flag); | |
| 436 | 41 | } else if (flag == "--worlds") { | |
| 437 | 3 | command.worlds = ParseNumber(value(), flag); | |
| 438 | 38 | } else if (flag == "--camera") { | |
| 439 | 2 | command.camera = value(); | |
| 440 | 36 | } else if (flag == "--product") { | |
| 441 | 2 | command.product = value(); | |
| 442 | 34 | } else if (flag == "--preset") { | |
| 443 | 4 | command.preset = value(); | |
| 444 | 4 | if (command.preset != "real-time" && command.preset != "minimal") { | |
| 445 | 1 | throw Error("--preset takes real-time or minimal, not \"{}\"", command.preset); | |
| 446 | } | ||
| 447 | 30 | } else if (flag == "--aovs") { | |
| 448 | 3 | std::string_view list = value(); | |
| 449 | 8 | while (!list.empty()) { | |
| 450 | 5 | const size_t comma = std::min(list.find(','), list.size()); | |
| 451 | 5 | if (comma > 0) { | |
| 452 | 4 | command.aovs.emplace_back(list.substr(0, comma)); | |
| 453 | } | ||
| 454 | 8 | list.remove_prefix(std::min(comma + 1, list.size())); | |
| 455 | } | ||
| 456 | 27 | } else if (flag == "--headless") { | |
| 457 | headless = true; | ||
| 458 | 23 | } else if (flag == "--frames") { | |
| 459 | 3 | command.frames = ParseNumber(value(), flag); | |
| 460 | 3 | headless = true; | |
| 461 | 20 | } else if (flag == "--output") { | |
| 462 | 3 | command.output = value(); | |
| 463 | 3 | headless = true; | |
| 464 | 17 | } else if (flag == "--benchmark") { | |
| 465 | benchmark = true; | ||
| 466 | 13 | } else if (flag == "--warmup") { | |
| 467 | 3 | command.warmup = ParseNumber(value(), flag); | |
| 468 | 3 | benchmarkOnly = benchmarkOnly.empty() ? std::string(flag) : benchmarkOnly; | |
| 469 | 10 | } else if (flag == "--json") { | |
| 470 | 3 | command.json = true; | |
| 471 | 3 | benchmarkOnly = benchmarkOnly.empty() ? std::string(flag) : benchmarkOnly; | |
| 472 | 7 | } else if (flag == "--profile") { | |
| 473 | 1 | command.profile = true; | |
| 474 | 6 | } else if (flag == "--vsync") { | |
| 475 | 2 | command.vsync = true; | |
| 476 | 4 | } else if (flag == "--aov") { | |
| 477 | 1 | command.aov = value(); | |
| 478 | 3 | } else if (flag == "--layout") { | |
| 479 | 15 | command.layout = value(); | |
| 480 | 2 | } else if (flag == "--xr" || flag == "--xr-sim") { | |
| 481 | #ifndef CHRONO_BATCH_RENDER_APP_XR | ||
| 482 | 1 | throw Error("This Chrono Batch Render was built without XR, so it has no {}", flag); | |
| 483 | #endif | ||
| 484 | const XrMode mode = flag == "--xr" ? XrMode::OpenXR : XrMode::Simulated; | ||
| 485 | if (command.xr != XrMode::Off && command.xr != mode) { | ||
| 486 | throw Error("--xr and --xr-sim can't be used together"); | ||
| 487 | } | ||
| 488 | command.xr = mode; | ||
| 489 | 1 | } else if (flag == "--xr-record" || flag == "--xr-replay") { | |
| 490 | #ifndef CHRONO_BATCH_RENDER_APP_XR | ||
| 491 | ✗ | throw Error("This Chrono Batch Render was built without XR, so it has no {}", flag); | |
| 492 | #endif | ||
| 493 | (flag == "--xr-record" ? command.xrRecord : command.xrReplay) = value(); | ||
| 494 | } else { | ||
| 495 | 1 | throw Error("Unknown option \"{}\"; --help lists them", args[at]); | |
| 496 | } | ||
| 497 | 58 | if (attached && !taken) { | |
| 498 | 1 | throw Error("{} takes no value", flag); | |
| 499 | } | ||
| 500 | } | ||
| 501 | 28 | if (!benchmarkOnly.empty() && !benchmark) { | |
| 502 | 2 | throw Error("{} goes with --benchmark", benchmarkOnly); | |
| 503 | } | ||
| 504 | 26 | if (benchmark && !command.output.empty()) { | |
| 505 | 2 | throw Error("--benchmark writes no images, so it takes no --output"); | |
| 506 | } | ||
| 507 | 25 | if (!command.xrReplay.empty()) { | |
| 508 | ✗ | if (command.xr == XrMode::OpenXR) { | |
| 509 | ✗ | throw Error("--xr-replay plays back on the simulated headset, so it takes no --xr"); | |
| 510 | } | ||
| 511 | ✗ | command.xr = XrMode::Simulated; | |
| 512 | } | ||
| 513 | 50 | if (!command.xrRecord.empty() && command.xr == XrMode::Off) { | |
| 514 | ✗ | throw Error("--xr-record goes with --xr, --xr-sim or --xr-replay"); | |
| 515 | } | ||
| 516 | 25 | if (command.xr != XrMode::Off && (benchmark || !command.output.empty())) { | |
| 517 | ✗ | throw Error("XR renders to the headset, so it takes no {}", | |
| 518 | ✗ | benchmark ? "--benchmark" : "--output"); | |
| 519 | } | ||
| 520 | 25 | if (command.kind == Command::Kind::Viewer && command.xr != XrMode::Off && headless) { | |
| 521 | ✗ | command.kind = Command::Kind::Xr; | |
| 522 | 25 | } else if (command.kind == Command::Kind::Viewer && (headless || benchmark)) { | |
| 523 | 16 | command.kind = benchmark ? Command::Kind::Benchmark : Command::Kind::Render; | |
| 524 | } | ||
| 525 | 25 | return command; | |
| 526 | 39 | } | |
| 527 | |||
| 528 | 8 | int RunCommand(const Command& command) { | |
| 529 | 8 | if (command.logLevel) { | |
| 530 | ✗ | SetLogLevel(*command.logLevel); | |
| 531 | } | ||
| 532 | 8 | switch (command.kind) { | |
| 533 | 1 | case Command::Kind::Help: | |
| 534 | 1 | std::fputs(kHelp, stdout); | |
| 535 | return 0; | ||
| 536 | 1 | case Command::Kind::Version: { | |
| 537 | 1 | const BuildInfo& build = GetBuildInfo(); | |
| 538 | 6 | std::printf("chrono-batch-render %.*s (%.*s%s, %.*s)\n", int(build.fullVersion.size()), | |
| 539 | 1 | build.fullVersion.data(), int(build.commit.size()), build.commit.data(), | |
| 540 | 1 | build.modified ? "+" : "", int(build.commitDate.size()), | |
| 541 | build.commitDate.data()); | ||
| 542 | return 0; | ||
| 543 | } | ||
| 544 | 1 | case Command::Kind::Devices: | |
| 545 | 1 | PrintDevices(); | |
| 546 | return 0; | ||
| 547 | ✗ | case Command::Kind::Xr: | |
| 548 | #ifdef CHRONO_BATCH_RENDER_APP_XR | ||
| 549 | return RunXr(command); | ||
| 550 | #else | ||
| 551 | ✗ | throw Error("This Chrono Batch Render was built without XR"); | |
| 552 | #endif | ||
| 553 | case Command::Kind::Viewer: | ||
| 554 | case Command::Kind::Render: | ||
| 555 | case Command::Kind::Benchmark: | ||
| 556 | break; | ||
| 557 | } | ||
| 558 | |||
| 559 | 5 | const bool viewer = command.kind == Command::Kind::Viewer; | |
| 560 | 5 | RunViewer run = nullptr; | |
| 561 | 8 | device::DeviceDesc desc = command.device; | |
| 562 | 5 | if (viewer) { | |
| 563 | ✗ | run = FindViewer(); | |
| 564 | ✗ | if (run == nullptr) { | |
| 565 | ✗ | throw Error("This Chrono Batch Render has no viewer. Install it with the " | |
| 566 | "chrono-batch-render-viewer package or " | ||
| 567 | ✗ | "the chrono-batch-render wheel, or render with --headless"); | |
| 568 | } | ||
| 569 | ✗ | if (desc.adapter.empty()) { | |
| 570 | ✗ | desc.backend = device::Backend::Vulkan; | |
| 571 | } | ||
| 572 | ✗ | for (const device::AdapterInfo& adapter : device::EnumerateAdapters()) { | |
| 573 | ✗ | if (adapter.alias == desc.adapter) { | |
| 574 | ✗ | desc.backend = adapter.backend; | |
| 575 | } | ||
| 576 | ✗ | } | |
| 577 | ✗ | desc.presentation = desc.backend == device::Backend::Vulkan; | |
| 578 | } | ||
| 579 | 10 | const Ref<device::Device> device = device::Device::Create(desc); | |
| 580 | 10 | const Ref<Scene> scene = DefaultScene(*device, command.worlds, !viewer); | |
| 581 | 7 | const std::string product = ChooseProduct(*scene, command); | |
| 582 | 2 | if (command.kind == Command::Kind::Render) { | |
| 583 | 1 | return RenderFrames(scene, product, command); | |
| 584 | } | ||
| 585 | 1 | if (command.kind == Command::Kind::Benchmark) { | |
| 586 | 1 | return Benchmark(scene, product, command); | |
| 587 | } | ||
| 588 | ✗ | std::string error; | |
| 589 | ✗ | const int status = run(&command, scene.Get(), &product, &error); | |
| 590 | ✗ | if (!error.empty()) { | |
| 591 | ✗ | throw Error("{}", error); | |
| 592 | } | ||
| 593 | ✗ | return status; | |
| 594 | } | ||
| 595 | |||
| 596 | 4 | int RunCli(const std::vector<std::string>& args) { | |
| 597 | 4 | Command command; | |
| 598 | 4 | try { | |
| 599 | 4 | command = ParseCommand(args); | |
| 600 | 1 | } catch (const std::exception& error) { | |
| 601 | 1 | std::fprintf(stderr, "chrono-batch-render: %s\n", error.what()); | |
| 602 | 1 | return 2; | |
| 603 | 1 | } | |
| 604 | 3 | int status = 1; | |
| 605 | 3 | try { | |
| 606 | 3 | status = RunCommand(command); | |
| 607 | ✗ | } catch (const std::exception& error) { | |
| 608 | ✗ | std::fprintf(stderr, "chrono-batch-render: %s\n", error.what()); | |
| 609 | ✗ | } | |
| 610 | 3 | if (!device::CurrentBuilds().Done()) { | |
| 611 | ✗ | std::fflush(nullptr); | |
| 612 | ✗ | std::_Exit(status); | |
| 613 | } | ||
| 614 | return status; | ||
| 615 | 4 | } | |
| 616 | |||
| 617 | } | ||
| 618 |