chrono_batch_render/
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
283 / 342
82.7%
Branches:
334 / 464
72.0%
Functions:
13 / 16
81.2%

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
4/6
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 14 times.
16 if (error != std::errc() || end != text.data() + text.size() || number == 0) {
93
1/1
✓ Branch 2 taken 2 times.
2 throw Error("{} takes a number above 0, not \"{}\"", flag, text);
94 }
95 14 return number;
96 }
97
98
2/4
chrono::batch_render::device::Sampling chrono::batch_render::app::(anonymous namespace)::ParseMode<chrono::batch_render::device::Sampling>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >):
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
chrono::batch_render::device::Traversal chrono::batch_render::app::(anonymous namespace)::ParseMode<chrono::batch_render::device::Traversal>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >):
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
3 template <class Mode> Mode ParseMode(std::string_view text, std::string_view flag) {
99
2/4
chrono::batch_render::device::Sampling chrono::batch_render::app::(anonymous namespace)::ParseMode<chrono::batch_render::device::Sampling>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >):
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
chrono::batch_render::device::Traversal chrono::batch_render::app::(anonymous namespace)::ParseMode<chrono::batch_render::device::Traversal>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >):
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
3 if (text == "auto") {
100 return Mode::Auto;
101 }
102
2/4
chrono::batch_render::device::Sampling chrono::batch_render::app::(anonymous namespace)::ParseMode<chrono::batch_render::device::Sampling>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >):
✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
chrono::batch_render::device::Traversal chrono::batch_render::app::(anonymous namespace)::ParseMode<chrono::batch_render::device::Traversal>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >):
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
3 if (text == "hardware") {
103 return Mode::Hardware;
104 }
105
2/4
chrono::batch_render::device::Sampling chrono::batch_render::app::(anonymous namespace)::ParseMode<chrono::batch_render::device::Sampling>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >):
✗ Branch 0 not taken.
✗ Branch 1 not taken.
chrono::batch_render::device::Traversal chrono::batch_render::app::(anonymous namespace)::ParseMode<chrono::batch_render::device::Traversal>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >):
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1 time.
2 if (text == "software") {
106 return Mode::Software;
107 }
108
1/2
chrono::batch_render::device::Sampling chrono::batch_render::app::(anonymous namespace)::ParseMode<chrono::batch_render::device::Sampling>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >):
✗ Branch 2 not taken.
chrono::batch_render::device::Traversal chrono::batch_render::app::(anonymous namespace)::ParseMode<chrono::batch_render::device::Traversal>(std::basic_string_view<char, std::char_traits<char> >, std::basic_string_view<char, std::char_traits<char> >):
✓ Branch 2 taken 1 time.
1 throw Error("{} takes auto, hardware or software, not \"{}\"", flag, text);
109 }
110
111 1 void PrintDevices() {
112
2/2
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 1 time.
2 for (const device::AdapterInfo& adapter : device::EnumerateAdapters()) {
113
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
2 std::printf(
114 "%-10s %s, %.1f GiB%s%s%s\n", adapter.alias.c_str(), adapter.name.c_str(),
115
1/1
✓ Branch 1 taken 1 time.
1 double(adapter.memoryBytes) / double(1ull << 30),
116
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
1 adapter.features.presentation ? ", presents" : "",
117
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
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
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1 time.
4 return world == 0 ? std::string("/World") : fmt::format("/World_{}", world);
125 }
126
127 ✗ void PrintProfile() {
128
0/1
✗ Branch 2 not taken.
✗ const profiler::Report report = profiler::Summarize(profiler::Frames());
129
0/2
✗ Branch 1 not taken.
✗ Branch 4 not taken.
✗ 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
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 4 times.
5 if (worlds > 1) {
135
1/1
✓ Branch 1 taken 1 time.
1 scene->Define<World>("/World");
136 } else {
137
1/1
✓ Branch 1 taken 4 times.
4 scene->Define<Xform>("/World");
138 }
139
1/1
✓ Branch 1 taken 5 times.
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
3/3
✓ Branch 1 taken 25 times.
✓ Branch 3 taken 25 times.
✓ Branch 4 taken 5 times.
30 for (const auto& [name, color] : looks) {
146
1/1
✓ Branch 1 taken 25 times.
25 const std::string look = fmt::format("/World/Looks/{}", name);
147
1/1
✓ Branch 1 taken 25 times.
25 const Prim material = scene->Define<Material>(look);
148
4/4
✓ Branch 1 taken 25 times.
✓ Branch 4 taken 25 times.
✓ Branch 7 taken 25 times.
✓ Branch 12 taken 25 times.
25 const Prim surface = scene->Define<Shader>(look + "/Surface", {.id = "UsdPreviewSurface"});
149 25 surface.Set(UsdPreviewSurface::kDiffuseColor, color);
150
1/1
✓ Branch 1 taken 25 times.
25 material.Connect("surface", surface, "surface");
151 25 }
152
1/1
✓ Branch 1 taken 5 times.
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
3/3
✓ Branch 1 taken 5 times.
✓ Branch 4 taken 5 times.
✓ Branch 8 taken 5 times.
5 scene->Define<Cube>("/World/Cube",
158 {.materialBinding = {"/World/Looks/Red"}, .translate = {-6, 0, 1}});
159
2/2
✓ Branch 1 taken 5 times.
✓ Branch 5 taken 5 times.
5 scene->Define<Sphere>("/World/Sphere",
160 {.materialBinding = {"/World/Looks/Blue"}, .translate = {-3, 0, 1}});
161
2/2
✓ Branch 1 taken 5 times.
✓ Branch 5 taken 5 times.
5 scene->Define<Cylinder>("/World/Cylinder",
162 {.materialBinding = {"/World/Looks/Green"}, .translate = {0, 0, 1}});
163
2/2
✓ Branch 1 taken 5 times.
✓ Branch 5 taken 5 times.
5 scene->Define<Cone>("/World/Cone",
164 {.materialBinding = {"/World/Looks/Yellow"}, .translate = {3, 0, 1}});
165
2/2
✓ Branch 1 taken 5 times.
✓ Branch 5 taken 5 times.
5 scene->Define<Capsule>("/World/Capsule",
166 {.materialBinding = {"/World/Looks/Purple"}, .translate = {6, 0, 1}});
167
1/1
✓ Branch 1 taken 5 times.
5 scene->Define<DistantLight>("/World/Sun", {.orient = FromAzimuthElevation(146, 44)});
168
169 5 const Double3 eye{0, -15.6, 3.4};
170
3/3
✓ Branch 1 taken 5 times.
✓ Branch 4 taken 5 times.
✓ Branch 8 taken 5 times.
10 scene->Define<Camera>("/World/Camera", {.translate = eye,
171
1/1
✓ Branch 1 taken 5 times.
5 .orient = LookAt(eye, {0, 0, 1}),
172 .focalLength = 29,
173 .horizontalAperture = 24.0f * 16 / 9,
174 .verticalAperture = 24});
175
176
1/1
✓ Branch 1 taken 5 times.
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
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 4 times.
5 if (!command.product.empty()) {
182 1 const std::optional<Prim> product = scene.Find(command.product);
183
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
1 if (!product || !product->IsA(SchemaId::RenderProduct)) {
184
1/1
✓ Branch 2 taken 1 time.
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
1/8
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
4 if (viewer && command.camera.empty() && command.preset.empty() && command.aovs.empty()) {
190 ✗ return {};
191 }
192
193 4 std::vector<std::string> cameras;
194
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 3 times.
4 if (!command.camera.empty()) {
195
1/1
✓ Branch 1 taken 1 time.
1 const std::optional<Prim> camera = scene.Find(command.camera);
196
1/5
✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
✗ Branch 3 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
1 if (!camera || !camera->IsA(SchemaId::Camera)) {
197
1/1
✓ Branch 2 taken 1 time.
1 throw Error("The scene has no camera {}", command.camera);
198 }
199
0/1
✗ Branch 1 not taken.
✗ cameras.push_back(command.camera);
200 } else {
201
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 3 times.
7 for (uint32_t world = 0; world < command.worlds; ++world) {
202
2/2
✓ Branch 1 taken 4 times.
✓ Branch 4 taken 4 times.
8 cameras.push_back(WorldPath(world) + "/Camera");
203 }
204 }
205
206 3 scene.Define<Scope>("/Render");
207 3 std::vector<std::string> vars;
208
1/1
✓ Branch 1 taken 3 times.
3 std::vector<std::string> names = {"LdrColor"};
209
1/1
✓ Branch 1 taken 3 times.
3 names.insert(names.end(), command.aovs.begin(), command.aovs.end());
210
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 2 times.
7 for (const std::string& name : names) {
211
1/1
✓ Branch 1 taken 5 times.
5 const std::string path = "/Render/" + name;
212
1/2
✓ Branch 1 taken 5 times.
✗ Branch 2 not taken.
5 if (std::ranges::find(vars, path) == vars.end()) {
213
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 4 times.
5 if (!FindRenderVarSource(name)) {
214 1 throw Error("\"{}\" is no AOV; try LdrColor, HdrColor, DistanceToCamera or Normal",
215
1/1
✓ Branch 2 taken 1 time.
1 name);
216 }
217
1/1
✓ Branch 1 taken 4 times.
5 scene.Define<RenderVar>(path, {.sourceName = name});
218
1/1
✓ Branch 1 taken 4 times.
4 vars.push_back(path);
219 }
220 5 }
221
1/1
✓ Branch 1 taken 2 times.
2 const std::string product = "/Render/Product";
222
2/2
✓ Branch 1 taken 2 times.
✓ Branch 5 taken 2 times.
2 scene.Define<RenderProduct>(
223 product,
224
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
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/5
✓ Branch 2 taken 1 time.
✗ Branch 4 not taken.
✓ Branch 5 taken 1 time.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
2 Renderer renderer(scene);
233
1/1
✓ Branch 1 taken 1 time.
1 profiler::SetEnabled(command.profile);
234
1/2
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
1 const uint32_t frames = std::max(command.frames, 1u);
235 1 RenderOutput output;
236
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1 time.
2 for (uint32_t frame = 0; frame < frames; ++frame) {
237
1/1
✓ Branch 1 taken 1 time.
1 profiler::BeginFrame();
238
1/1
✓ Branch 1 taken 1 time.
1 scene->Advance(kStep);
239
1/1
✓ Branch 1 taken 1 time.
1 output = renderer.Step(product);
240 }
241
1/1
✓ Branch 1 taken 1 time.
1 output.Wait();
242
1/2
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
1 if (!command.output.empty()) {
243
1/1
✓ Branch 1 taken 1 time.
2 for (const std::filesystem::path& written :
244
4/4
✓ Branch 1 taken 1 time.
✓ Branch 4 taken 1 time.
✓ Branch 6 taken 4 times.
✓ Branch 7 taken 1 time.
5 WriteFrame(command.output, output[product].frames.back())) {
245
2/2
✓ Branch 1 taken 4 times.
✓ Branch 4 taken 4 times.
8 Log(LogLevel::Info, "wrote {}", written.string());
246 1 }
247 }
248
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
1 if (command.profile) {
249
0/1
✗ Branch 1 not taken.
✗ 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/5
✓ Branch 2 taken 1 time.
✗ Branch 4 not taken.
✓ Branch 5 taken 1 time.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
2 Renderer renderer(scene);
257
1/2
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
1 const uint32_t steps = command.frames > 0 ? command.frames : 100;
258 1 int64_t pixels = 0;
259
3/4
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 1 time.
2 for (uint32_t step = 0; step < std::max(command.warmup, 1u); ++step) {
260
1/1
✓ Branch 1 taken 1 time.
1 scene->Advance(kStep);
261
1/1
✓ Branch 1 taken 1 time.
1 const RenderOutput output = renderer.Step(product);
262
1/1
✓ Branch 1 taken 1 time.
1 output.Wait();
263
1/1
✓ Branch 1 taken 1 time.
1 const ProductOutput& rendered = output[product];
264
2/4
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
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/2
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
1 pixels = shape.size() >= 3 ? shape[0] * shape[1] * shape[2] : 0;
268 }
269 1 }
270
271
1/1
✓ Branch 1 taken 1 time.
1 profiler::SetEnabled(command.profile);
272 1 const Clock::time_point serialStart = Clock::now();
273
2/2
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 1 time.
3 for (uint32_t step = 0; step < steps; ++step) {
274
1/1
✓ Branch 1 taken 2 times.
2 profiler::BeginFrame();
275
1/1
✓ Branch 1 taken 2 times.
2 scene->Advance(kStep);
276
1/1
✓ Branch 1 taken 2 times.
4 renderer.Step(product).Wait();
277 }
278 1 const double latencyMs =
279
1/1
✓ Branch 3 taken 1 time.
1 std::chrono::duration<double, std::milli>(Clock::now() - serialStart).count() / steps;
280
281
1/1
✓ Branch 1 taken 1 time.
1 std::deque<RenderOutput> inFlight;
282 1 const Clock::time_point pipelinedStart = Clock::now();
283
2/2
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 1 time.
3 for (uint32_t step = 0; step < steps; ++step) {
284
1/1
✓ Branch 1 taken 2 times.
2 profiler::BeginFrame();
285
1/1
✓ Branch 1 taken 2 times.
2 scene->Advance(kStep);
286
1/1
✓ Branch 1 taken 2 times.
4 inFlight.push_back(renderer.Step(product));
287
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (inFlight.size() == 3) {
288
0/1
✗ Branch 1 not taken.
✗ inFlight.front().Wait();
289 ✗ inFlight.pop_front();
290 }
291 }
292
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 time.
3 for (const RenderOutput& output : inFlight) {
293
1/1
✓ Branch 1 taken 2 times.
4 output.Wait();
294 }
295
1/2
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
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/2
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
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/1
✓ Branch 1 taken 1 time.
1 product.c_str(), command.worlds, static_cast<long long>(pixels), command.warmup,
304 steps, latencyMs, fps, megapixels);
305 } else {
306
0/1
✗ Branch 1 not taken.
✗ std::printf("%s, %lld pixels a step\n", product.c_str(), static_cast<long long>(pixels));
307
0/1
✗ Branch 1 not taken.
✗ std::printf(" one step at a time %.3f ms\n", latencyMs);
308
0/1
✗ Branch 1 not taken.
✗ std::printf(" three in flight %.1f steps a second, %.1f megapixels a second\n", fps,
309 megapixels);
310 }
311
1/1
✓ Branch 1 taken 1 time.
1 std::fflush(stdout);
312
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
1 if (command.profile) {
313
0/1
✗ Branch 1 not taken.
✗ 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
0/4
✗ Branch 1 not taken.
✗ Branch 4 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ const std::string names[] = {stem + ".so." CHRONO_BATCH_RENDER_SOVERSION, stem + ".so"};
339 #endif
340
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ for (const std::string& candidate : names) {
341
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ 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
2/2
✓ Branch 0 taken 68 times.
✓ Branch 1 taken 28 times.
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
4/4
✓ Branch 1 taken 61 times.
✓ Branch 2 taken 7 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 59 times.
68 if (const size_t equals = flag.find('='); flag.starts_with("--") && equals != flag.npos) {
393
1/1
✓ Branch 1 taken 2 times.
2 attached = flag.substr(equals + 1);
394
1/1
✓ Branch 1 taken 2 times.
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
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 35 times.
36 if (attached) {
400 1 return *attached;
401 }
402
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 34 times.
35 if (++at == args.size()) {
403
1/1
✓ Branch 2 taken 1 time.
1 throw Error("{} needs a value", flag);
404 }
405 34 return args[at];
406 68 };
407
4/4
✓ Branch 0 taken 67 times.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 65 times.
✓ Branch 3 taken 2 times.
68 if (flag == "-h" || flag == "--help") {
408 3 command.kind = Command::Kind::Help;
409
8/8
✓ Branch 0 taken 62 times.
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 61 times.
✓ Branch 3 taken 1 time.
✓ Branch 4 taken 59 times.
✓ Branch 5 taken 2 times.
✓ Branch 6 taken 58 times.
✓ Branch 7 taken 1 time.
65 } else if (flag == "-v" || flag == "--verbose" || flag == "-q" || flag == "--quiet") {
410
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 3 times.
7 const LogLevel level =
411
4/4
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 3 times.
7 flag == "-v" || flag == "--verbose" ? LogLevel::Debug : LogLevel::Warning;
412
4/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 5 times.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 1 time.
7 if (command.logLevel && *command.logLevel != level) {
413
2/2
✓ Branch 2 taken 1 time.
✓ Branch 5 taken 1 time.
2 throw Error("--verbose and --quiet can't be used together");
414 }
415 6 command.logLevel = level;
416
4/4
✓ Branch 0 taken 57 times.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 55 times.
✓ Branch 3 taken 2 times.
58 } else if (flag == "-V" || flag == "--version") {
417 3 command.kind = Command::Kind::Version;
418
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 53 times.
55 } else if (flag == "--devices") {
419 2 command.kind = Command::Kind::Devices;
420
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 51 times.
53 } else if (flag == "--device") {
421
2/2
✓ Branch 1 taken 1 time.
✓ Branch 4 taken 1 time.
2 command.device.adapter = value();
422
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 50 times.
51 } else if (flag == "--validation") {
423 1 command.device.validation = true;
424
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 48 times.
50 } else if (flag == "--traversal") {
425
2/2
✓ Branch 1 taken 2 times.
✓ Branch 4 taken 1 time.
2 command.device.traversal = ParseMode<device::Traversal>(value(), flag);
426
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 47 times.
48 } else if (flag == "--sampling") {
427
2/2
✓ Branch 1 taken 1 time.
✓ Branch 4 taken 1 time.
1 command.device.sampling = ParseMode<device::Sampling>(value(), flag);
428
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 41 times.
47 } else if (flag == "--size") {
429
1/1
✓ Branch 1 taken 6 times.
6 const std::string_view size = value();
430 6 const size_t x = size.find('x');
431
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 4 times.
6 if (x == size.npos) {
432
1/1
✓ Branch 2 taken 2 times.
2 throw Error("--size takes WxH, such as 1600x900, not \"{}\"", size);
433 }
434
2/2
✓ Branch 1 taken 4 times.
✓ Branch 4 taken 3 times.
4 command.width = ParseNumber(size.substr(0, x), flag);
435
2/2
✓ Branch 1 taken 3 times.
✓ Branch 4 taken 3 times.
3 command.height = ParseNumber(size.substr(x + 1), flag);
436
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 38 times.
41 } else if (flag == "--worlds") {
437
2/2
✓ Branch 1 taken 3 times.
✓ Branch 4 taken 2 times.
3 command.worlds = ParseNumber(value(), flag);
438
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 36 times.
38 } else if (flag == "--camera") {
439
2/2
✓ Branch 1 taken 2 times.
✓ Branch 4 taken 2 times.
2 command.camera = value();
440
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 34 times.
36 } else if (flag == "--product") {
441
2/2
✓ Branch 1 taken 2 times.
✓ Branch 4 taken 2 times.
2 command.product = value();
442
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 30 times.
34 } else if (flag == "--preset") {
443
2/2
✓ Branch 1 taken 4 times.
✓ Branch 4 taken 4 times.
4 command.preset = value();
444
3/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 3 times.
4 if (command.preset != "real-time" && command.preset != "minimal") {
445
1/1
✓ Branch 2 taken 1 time.
1 throw Error("--preset takes real-time or minimal, not \"{}\"", command.preset);
446 }
447
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 27 times.
30 } else if (flag == "--aovs") {
448
1/1
✓ Branch 1 taken 3 times.
3 std::string_view list = value();
449
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 3 times.
8 while (!list.empty()) {
450
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 2 times.
5 const size_t comma = std::min(list.find(','), list.size());
451
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 1 time.
5 if (comma > 0) {
452
2/2
✓ Branch 1 taken 4 times.
✓ Branch 4 taken 4 times.
4 command.aovs.emplace_back(list.substr(0, comma));
453 }
454
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 2 times.
8 list.remove_prefix(std::min(comma + 1, list.size()));
455 }
456
2/2
✓ Branch 0 taken 23 times.
✓ Branch 1 taken 4 times.
27 } else if (flag == "--headless") {
457 headless = true;
458
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 20 times.
23 } else if (flag == "--frames") {
459
2/2
✓ Branch 1 taken 3 times.
✓ Branch 4 taken 3 times.
3 command.frames = ParseNumber(value(), flag);
460 3 headless = true;
461
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 17 times.
20 } else if (flag == "--output") {
462
2/2
✓ Branch 1 taken 3 times.
✓ Branch 4 taken 3 times.
3 command.output = value();
463 3 headless = true;
464
2/2
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 4 times.
17 } else if (flag == "--benchmark") {
465 benchmark = true;
466
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 10 times.
13 } else if (flag == "--warmup") {
467
2/2
✓ Branch 1 taken 3 times.
✓ Branch 4 taken 3 times.
3 command.warmup = ParseNumber(value(), flag);
468
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 3 taken 3 times.
✗ Branch 6 not taken.
3 benchmarkOnly = benchmarkOnly.empty() ? std::string(flag) : benchmarkOnly;
469
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 7 times.
10 } else if (flag == "--json") {
470 3 command.json = true;
471
4/4
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 2 times.
✓ Branch 3 taken 1 time.
✓ Branch 6 taken 2 times.
3 benchmarkOnly = benchmarkOnly.empty() ? std::string(flag) : benchmarkOnly;
472
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 6 times.
7 } else if (flag == "--profile") {
473 1 command.profile = true;
474
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 4 times.
6 } else if (flag == "--vsync") {
475 2 command.vsync = true;
476
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 3 times.
4 } else if (flag == "--aov") {
477
2/2
✓ Branch 1 taken 1 time.
✓ Branch 4 taken 1 time.
1 command.aov = value();
478
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 2 times.
3 } else if (flag == "--layout") {
479
2/2
✓ Branch 1 taken 1 time.
✓ Branch 4 taken 1 time.
15 command.layout = value();
480
3/4
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
2 } else if (flag == "--xr" || flag == "--xr-sim") {
481 #ifndef CHRONO_BATCH_RENDER_APP_XR
482
1/1
✓ Branch 2 taken 1 time.
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
2/4
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
1 } else if (flag == "--xr-record" || flag == "--xr-replay") {
490 #ifndef CHRONO_BATCH_RENDER_APP_XR
491
0/1
✗ Branch 2 not taken.
✗ 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/1
✓ Branch 2 taken 1 time.
1 throw Error("Unknown option \"{}\"; --help lists them", args[at]);
496 }
497
4/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 56 times.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 1 time.
58 if (attached && !taken) {
498
1/1
✓ Branch 2 taken 1 time.
1 throw Error("{} takes no value", flag);
499 }
500 }
501
4/4
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 24 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 2 times.
28 if (!benchmarkOnly.empty() && !benchmark) {
502
1/1
✓ Branch 2 taken 2 times.
2 throw Error("{} goes with --benchmark", benchmarkOnly);
503 }
504
4/4
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 22 times.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 1 time.
26 if (benchmark && !command.output.empty()) {
505
2/2
✓ Branch 2 taken 1 time.
✓ Branch 5 taken 1 time.
2 throw Error("--benchmark writes no images, so it takes no --output");
506 }
507
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 25 times.
25 if (!command.xrReplay.empty()) {
508
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ if (command.xr == XrMode::OpenXR) {
509
0/2
✗ Branch 2 not taken.
✗ Branch 5 not taken.
✗ throw Error("--xr-replay plays back on the simulated headset, so it takes no --xr");
510 }
511 ✗ command.xr = XrMode::Simulated;
512 }
513
2/6
✗ Branch 0 not taken.
✓ Branch 1 taken 25 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 25 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
50 if (!command.xrRecord.empty() && command.xr == XrMode::Off) {
514
0/2
✗ Branch 2 not taken.
✗ Branch 5 not taken.
✗ throw Error("--xr-record goes with --xr, --xr-sim or --xr-replay");
515 }
516
1/6
✗ Branch 0 not taken.
✓ Branch 1 taken 25 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
25 if (command.xr != XrMode::Off && (benchmark || !command.output.empty())) {
517 ✗ throw Error("XR renders to the headset, so it takes no {}",
518
0/3
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 4 not taken.
✗ benchmark ? "--benchmark" : "--output");
519 }
520
3/6
✓ Branch 0 taken 17 times.
✓ Branch 1 taken 8 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 17 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
25 if (command.kind == Command::Kind::Viewer && command.xr != XrMode::Off && headless) {
521 ✗ command.kind = Command::Kind::Xr;
522
4/4
✓ Branch 0 taken 17 times.
✓ Branch 1 taken 8 times.
✓ Branch 2 taken 9 times.
✓ Branch 3 taken 8 times.
25 } else if (command.kind == Command::Kind::Viewer && (headless || benchmark)) {
523
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 2 times.
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
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (command.logLevel) {
530 ✗ SetLogLevel(*command.logLevel);
531 }
532
4/5
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
✓ Branch 4 taken 5 times.
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
1/2
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
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/2
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
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
0/2
✗ Branch 2 not taken.
✗ Branch 5 not taken.
✗ 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
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 if (viewer) {
563
0/1
✗ Branch 1 not taken.
✗ run = FindViewer();
564
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ 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
0/2
✗ Branch 2 not taken.
✗ Branch 5 not taken.
✗ "the chrono-batch-render wheel, or render with --headless");
568 }
569
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ if (desc.adapter.empty()) {
570 ✗ desc.backend = device::Backend::Vulkan;
571 }
572
0/3
✗ Branch 1 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ for (const device::AdapterInfo& adapter : device::EnumerateAdapters()) {
573
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ if (adapter.alias == desc.adapter) {
574 ✗ desc.backend = adapter.backend;
575 }
576 ✗ }
577
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ desc.presentation = desc.backend == device::Backend::Vulkan;
578 }
579
1/1
✓ Branch 1 taken 5 times.
10 const Ref<device::Device> device = device::Device::Create(desc);
580
2/3
✓ Branch 1 taken 5 times.
✓ Branch 3 taken 2 times.
✗ Branch 4 not taken.
10 const Ref<Scene> scene = DefaultScene(*device, command.worlds, !viewer);
581
2/3
✓ Branch 1 taken 2 times.
✓ Branch 3 taken 2 times.
✗ Branch 4 not taken.
7 const std::string product = ChooseProduct(*scene, command);
582
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1 time.
2 if (command.kind == Command::Kind::Render) {
583
1/1
✓ Branch 1 taken 1 time.
1 return RenderFrames(scene, product, command);
584 }
585
1/2
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
1 if (command.kind == Command::Kind::Benchmark) {
586
1/1
✓ Branch 1 taken 1 time.
1 return Benchmark(scene, product, command);
587 }
588
0/1
✗ Branch 1 not taken.
✗ std::string error;
589
0/1
✗ Branch 1 not taken.
✗ const int status = run(&command, scene.Get(), &product, &error);
590
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ if (!error.empty()) {
591
0/1
✗ Branch 2 not taken.
✗ 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
1/1
✓ Branch 1 taken 3 times.
4 command = ParseCommand(args);
600
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
1 } catch (const std::exception& error) {
601
1/1
✓ Branch 2 taken 1 time.
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
1/1
✓ Branch 1 taken 3 times.
3 status = RunCommand(command);
607
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ } catch (const std::exception& error) {
608
0/1
✗ Branch 2 not taken.
✗ std::fprintf(stderr, "chrono-batch-render: %s\n", error.what());
609 ✗ }
610
2/3
✓ Branch 1 taken 3 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 3 times.
3 if (!device::CurrentBuilds().Done()) {
611
0/1
✗ Branch 1 not taken.
✗ std::fflush(nullptr);
612 ✗ std::_Exit(status);
613 }
614 return status;
615 4 }
616
617 }
618