device/commands.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // SPDX-License-Identifier: BSD-3-Clause | ||
| 2 | #include "chrono_batch_render/device/commands.h" | ||
| 3 | |||
| 4 | #include <algorithm> | ||
| 5 | #include <cstring> | ||
| 6 | #include <utility> | ||
| 7 | |||
| 8 | #include "chrono_batch_render/device/device.h" | ||
| 9 | #include "chrono_batch_render/device/kernels/shared.h" | ||
| 10 | #include "chrono_batch_render/device/queue.h" | ||
| 11 | #include "chrono_batch_render/util/error.h" | ||
| 12 | #include "chrono_batch_render/util/log.h" | ||
| 13 | |||
| 14 | namespace chrono::batch_render::device { | ||
| 15 | namespace { | ||
| 16 | |||
| 17 | 341734 | void RequireWords(BufferRange range, const char* what) { | |
| 18 | 341734 | if (range.Offset() % 4 != 0 || range.Bytes() % 4 != 0) { | |
| 19 | ✗ | throw Error("{} of buffer \"{}\" has offset {} and size {}; both must be " | |
| 20 | "multiples of 4", | ||
| 21 | ✗ | what, range.GetBuffer().Label(), range.Offset(), range.Bytes()); | |
| 22 | } | ||
| 23 | 341734 | } | |
| 24 | |||
| 25 | constexpr uint32_t kMaxGroups = kMaxGroupsPerAxis; | ||
| 26 | |||
| 27 | constexpr bool Covers(Size3 folded, uint32_t count) { | ||
| 28 | return folded.x <= kMaxGroups && folded.y <= kMaxGroups && folded.z <= kMaxGroups && | ||
| 29 | uint64_t(folded.x) * folded.y * folded.z >= count; | ||
| 30 | } | ||
| 31 | |||
| 32 | static_assert(FoldGroups(65536) == Size3{32768, 2, 1}); | ||
| 33 | static_assert(Covers(FoldGroups(65536), 65536)); | ||
| 34 | static_assert(Covers(FoldGroups(70001), 70001)); | ||
| 35 | static_assert(Covers(FoldGroups(uint32_t(kMaxGroups) * kMaxGroups), | ||
| 36 | uint32_t(kMaxGroups) * kMaxGroups)); | ||
| 37 | static_assert(FoldGroups(~uint32_t(0)).z == 2 && Covers(FoldGroups(~uint32_t(0)), ~uint32_t(0))); | ||
| 38 | |||
| 39 | 154685 | Size3 Grid(const Kernel& kernel, Size3 groups) { | |
| 40 | 154685 | if (groups.y > kMaxGroups || groups.z > kMaxGroups) { | |
| 41 | ✗ | throw Error("Dispatch of kernel {} over {} x {} x {} groups; y and z take at most {}", | |
| 42 | ✗ | kernel.Name(), groups.x, groups.y, groups.z, kMaxGroups); | |
| 43 | } | ||
| 44 | 154685 | if (groups.x <= kMaxGroups) { | |
| 45 | 154685 | return groups; | |
| 46 | } | ||
| 47 | ✗ | if (groups.y != 1 || groups.z != 1) { | |
| 48 | ✗ | throw Error("Dispatch of kernel {} over {} x {} x {} groups; only x takes more than {}, " | |
| 49 | "and only when y and z are 1", | ||
| 50 | ✗ | kernel.Name(), groups.x, groups.y, groups.z, kMaxGroups); | |
| 51 | } | ||
| 52 | ✗ | if (!kernel.Foldable()) { | |
| 53 | ✗ | throw Error("Dispatch of kernel {} over {} groups, which fold into more axes; it reads " | |
| 54 | "SV_DispatchThreadID or SV_GroupID, which folding changes: index it with " | ||
| 55 | "LinearThreadIndex()", | ||
| 56 | ✗ | kernel.Name(), groups.x); | |
| 57 | } | ||
| 58 | ✗ | return FoldGroups(groups.x); | |
| 59 | } | ||
| 60 | |||
| 61 | 77897 | void RequireCounts(const Kernel& kernel, BufferRange counts, Size3 groups) { | |
| 62 | 77897 | if (!Has(counts.GetBuffer().Usage(), BufferUsage::Indirect)) { | |
| 63 | ✗ | throw Error("Dispatch of kernel {} reads its counts from buffer \"{}\", which lacks " | |
| 64 | "BufferUsage::Indirect", | ||
| 65 | ✗ | kernel.Name(), counts.GetBuffer().Label()); | |
| 66 | } | ||
| 67 | 77897 | if (counts.Offset() % 4 != 0 || counts.Bytes() < 12) { | |
| 68 | ✗ | throw Error("Dispatch of kernel {} reads its counts at offset {} of {} bytes of buffer " | |
| 69 | "\"{}\"; they are 12 bytes at a multiple of 4", | ||
| 70 | ✗ | kernel.Name(), counts.Offset(), counts.Bytes(), counts.GetBuffer().Label()); | |
| 71 | } | ||
| 72 | 77897 | if (groups.x > kMaxGroups || groups.y > kMaxGroups || groups.z > kMaxGroups) { | |
| 73 | ✗ | throw Error("Dispatch of kernel {} over at most {} x {} x {} groups read; each count " | |
| 74 | "is at most {}, as they don't fold", | ||
| 75 | ✗ | kernel.Name(), groups.x, groups.y, groups.z, kMaxGroups); | |
| 76 | } | ||
| 77 | 77897 | } | |
| 78 | |||
| 79 | } | ||
| 80 | |||
| 81 | 238552 | CommandBuffer::CommandBuffer(Queue& queue, bool own) : m_queue(&queue), m_open(!own) { | |
| 82 | 238552 | if (!own) { | |
| 83 | 127290 | m_device = Ref<Device>(&queue.GetDevice()); | |
| 84 | 127290 | queue.OpenRecording(); | |
| 85 | } | ||
| 86 | 238552 | } | |
| 87 | |||
| 88 | 146660 | CommandBuffer::CommandBuffer(CommandBuffer&& other) noexcept | |
| 89 | 146660 | : m_queue(other.m_queue), m_device(std::move(other.m_device)), | |
| 90 | 146660 | m_commands(std::move(other.m_commands)), m_data(std::move(other.m_data)), | |
| 91 | 146660 | m_zones(std::move(other.m_zones)), m_open(std::exchange(other.m_open, false)), | |
| 92 | 146660 | m_reusable(other.m_reusable), m_held(std::move(other.m_held)), | |
| 93 | 146660 | m_submission(other.m_submission), m_recorded(std::move(other.m_recorded)) {} | |
| 94 | |||
| 95 | 148281 | CommandBuffer& CommandBuffer::operator=(CommandBuffer&& other) noexcept { | |
| 96 | 148281 | if (this != &other) { | |
| 97 | 148281 | Close(); | |
| 98 | 148281 | Release(); | |
| 99 | 148281 | m_queue = other.m_queue; | |
| 100 | 148281 | m_device = std::move(other.m_device); | |
| 101 | 148281 | m_commands = std::move(other.m_commands); | |
| 102 | 148281 | m_data = std::move(other.m_data); | |
| 103 | 148281 | m_zones = std::move(other.m_zones); | |
| 104 | 148281 | m_open = std::exchange(other.m_open, false); | |
| 105 | 148281 | m_reusable = other.m_reusable; | |
| 106 | 148281 | m_held = std::move(other.m_held); | |
| 107 | 148281 | m_submission = other.m_submission; | |
| 108 | 148281 | m_recorded = std::move(other.m_recorded); | |
| 109 | } | ||
| 110 | 148281 | return *this; | |
| 111 | } | ||
| 112 | |||
| 113 | 385212 | CommandBuffer::~CommandBuffer() { | |
| 114 | 385212 | Close(); | |
| 115 | 385212 | Release(); | |
| 116 | 385212 | } | |
| 117 | |||
| 118 | 533493 | void CommandBuffer::Release() { | |
| 119 | 533493 | if (m_recorded.native || m_recorded.timestamps) { | |
| 120 | 4 | m_queue->Retire(std::make_shared<Recorded>(std::exchange(m_recorded, {})), m_submission); | |
| 121 | } | ||
| 122 | 533493 | } | |
| 123 | |||
| 124 | 597130 | void CommandBuffer::Close() { | |
| 125 | 597130 | if (std::exchange(m_open, false)) { | |
| 126 | 127290 | m_queue->CloseRecording(); | |
| 127 | } | ||
| 128 | 597130 | } | |
| 129 | |||
| 130 | 2 | void CommandBuffer::HoldResources() { | |
| 131 | 7 | const auto hold = [&](Resource* resource) { | |
| 132 | 5 | if (resource != nullptr && | |
| 133 | 5 | std::none_of(m_held.begin(), m_held.end(), [resource](const Ref<Resource>& held) { | |
| 134 | 3 | return held.Get() == resource; | |
| 135 | })) { | ||
| 136 | 4 | m_held.emplace_back(resource); | |
| 137 | } | ||
| 138 | 7 | }; | |
| 139 | 8 | for (const Command& command : m_commands) { | |
| 140 | 6 | std::visit(Overloaded{ | |
| 141 | 2 | [&](const CopyCommand& c) { | |
| 142 | 2 | hold(c.src); | |
| 143 | 2 | hold(c.dst); | |
| 144 | 2 | }, | |
| 145 | ✗ | [&](const TextureCopyCommand& c) { | |
| 146 | ✗ | hold(c.src); | |
| 147 | ✗ | hold(c.dst); | |
| 148 | ✗ | }, | |
| 149 | 1 | [&](const FillCommand& c) { hold(c.dst); }, | |
| 150 | ✗ | [&](const UpdateCommand& c) { hold(c.dst); }, | |
| 151 | ✗ | [&](const DispatchCommand& c) { | |
| 152 | ✗ | hold(&c.kernel->GetProgram()); | |
| 153 | ✗ | hold(c.counts); | |
| 154 | ✗ | }, | |
| 155 | ✗ | [&](const BuildAccelCommand& c) { | |
| 156 | ✗ | hold(c.dst); | |
| 157 | ✗ | hold(c.scratch); | |
| 158 | ✗ | ForEachBuffer(c.input, | |
| 159 | ✗ | [&](BufferRange range) { hold(&range.GetBuffer()); }); | |
| 160 | ✗ | }, | |
| 161 | [&](const BarrierCommand&) {}, | ||
| 162 | [&](const ZoneCommand&) {}, | ||
| 163 | }, | ||
| 164 | command); | ||
| 165 | } | ||
| 166 | 2 | } | |
| 167 | |||
| 168 | 65916 | CommandEncoder::CommandEncoder(Queue& queue, bool own) : m_recording(queue, own), m_own(own) {} | |
| 169 | |||
| 170 | 12395 | void CommandEncoder::Append(const CommandBuffer& commands) { | |
| 171 | 12395 | const auto zones = uint32_t(m_recording.m_zones.size()); | |
| 172 | 12395 | m_recording.m_zones.insert(m_recording.m_zones.end(), commands.m_zones.begin(), | |
| 173 | commands.m_zones.end()); | ||
| 174 | 12395 | m_recording.m_commands.push_back(BarrierCommand{}); | |
| 175 | 741915 | for (Command command : commands.m_commands) { | |
| 176 | 729520 | std::visit(Overloaded{ | |
| 177 | 47537 | [&](UpdateCommand& c) { | |
| 178 | 47537 | c.data = Store(commands.m_data.data() + c.data, c.bytes, 4); | |
| 179 | 47537 | }, | |
| 180 | 265765 | [&](DispatchCommand& c) { | |
| 181 | 265765 | c.data = Store(commands.m_data.data() + c.data, c.paramBytes, 8); | |
| 182 | 265765 | }, | |
| 183 | 30200 | [&](ZoneCommand& c) { c.index += zones; }, | |
| 184 | [](auto&) {}, | ||
| 185 | }, | ||
| 186 | command); | ||
| 187 | 729520 | m_recording.m_commands.push_back(command); | |
| 188 | } | ||
| 189 | 12395 | m_barrier = true; | |
| 190 | 12395 | } | |
| 191 | |||
| 192 | 705653 | void CommandEncoder::Require(const Resource& resource, const char* what) const { | |
| 193 | 705653 | m_recording.m_queue->GetDevice().Require(resource, what); | |
| 194 | 705653 | } | |
| 195 | |||
| 196 | 755916 | size_t CommandEncoder::Store(const void* data, size_t bytes, size_t alignment) { | |
| 197 | 755916 | std::vector<std::byte>& stored = m_recording.m_data; | |
| 198 | 755916 | const size_t at = RoundUp(stored.size(), alignment); | |
| 199 | 755916 | stored.resize(at + RoundUp(bytes, 4)); | |
| 200 | 755916 | if (bytes > 0) { | |
| 201 | 755916 | std::memcpy(stored.data() + at, data, bytes); | |
| 202 | } | ||
| 203 | 755916 | return at; | |
| 204 | } | ||
| 205 | |||
| 206 | 601035 | void CommandEncoder::Record(const Command& command) { | |
| 207 | 601035 | if (!m_concurrent || m_barrier) { | |
| 208 | 601034 | m_recording.m_commands.push_back(BarrierCommand{}); | |
| 209 | 601034 | m_barrier = false; | |
| 210 | } | ||
| 211 | 601035 | m_recording.m_commands.push_back(command); | |
| 212 | 601035 | } | |
| 213 | |||
| 214 | 26590 | void CommandEncoder::Copy(BufferRange src, BufferRange dst) { | |
| 215 | 26590 | Require(src.GetBuffer(), "Copy"); | |
| 216 | 26590 | Require(dst.GetBuffer(), "Copy"); | |
| 217 | 26590 | if (dst.Bytes() < src.Bytes()) { | |
| 218 | ✗ | throw Error("Copy of {} bytes into {} bytes of buffer \"{}\"", src.Bytes(), dst.Bytes(), | |
| 219 | ✗ | dst.GetBuffer().Label()); | |
| 220 | } | ||
| 221 | 26590 | const bool sameBuffer = &src.GetBuffer() == &dst.GetBuffer(); | |
| 222 | 26590 | if (sameBuffer && src.Offset() < dst.Offset() + src.Bytes() && | |
| 223 | 14 | dst.Offset() < src.Offset() + src.Bytes()) { | |
| 224 | ✗ | throw Error("Copy within buffer \"{}\" overlaps itself", src.GetBuffer().Label()); | |
| 225 | } | ||
| 226 | 26590 | if (src.Bytes() > 0) { | |
| 227 | 51252 | const auto staging = [](const Buffer& buffer) { | |
| 228 | 49324 | return buffer.GetMemory() == Memory::Upload || buffer.GetMemory() == Memory::Readback; | |
| 229 | }; | ||
| 230 | 51252 | if (!staging(src.GetBuffer()) && !staging(dst.GetBuffer())) { | |
| 231 | 483 | m_recording.m_queue->GetDevice().CountTraffic({.copyBytes = src.Bytes()}); | |
| 232 | } | ||
| 233 | 26590 | Record(CopyCommand{&src.GetBuffer(), src.Offset(), &dst.GetBuffer(), dst.Offset(), | |
| 234 | 26590 | src.Bytes()}); | |
| 235 | } | ||
| 236 | 26590 | } | |
| 237 | |||
| 238 | 130 | void CommandEncoder::Copy(BufferRange src, Texture& dst, uint32_t level) { | |
| 239 | 130 | Require(src.GetBuffer(), "Copy"); | |
| 240 | 130 | Require(dst, "Copy"); | |
| 241 | 130 | if (level >= dst.Desc().levels) { | |
| 242 | ✗ | throw Error("Copy into level {} of texture \"{}\", which has {}", level, dst.Label(), | |
| 243 | ✗ | dst.Desc().levels); | |
| 244 | } | ||
| 245 | 130 | const uint64_t bytes = dst.LevelBytes(level); | |
| 246 | 130 | if (src.Bytes() < bytes) { | |
| 247 | ✗ | throw Error("Copy of {} bytes into level {} of texture \"{}\", which takes {}", src.Bytes(), | |
| 248 | ✗ | level, dst.Label(), bytes); | |
| 249 | } | ||
| 250 | 130 | if (src.Offset() % 16 != 0) { | |
| 251 | ✗ | throw Error("Copy into texture \"{}\" from offset {} of buffer \"{}\"; it takes a " | |
| 252 | "multiple of 16", | ||
| 253 | ✗ | dst.Label(), src.Offset(), src.GetBuffer().Label()); | |
| 254 | } | ||
| 255 | 130 | Record(TextureCopyCommand{&src.GetBuffer(), src.Offset(), &dst, level}); | |
| 256 | 130 | } | |
| 257 | |||
| 258 | 131701 | void CommandEncoder::Fill(BufferRange dst, uint32_t value) { | |
| 259 | 131701 | Require(dst.GetBuffer(), "Fill"); | |
| 260 | 131701 | RequireWords(dst, "Fill"); | |
| 261 | 131701 | if (dst.Bytes() > 0) { | |
| 262 | 131701 | Record(FillCommand{&dst.GetBuffer(), dst.Offset(), dst.Bytes(), value}); | |
| 263 | } | ||
| 264 | 131701 | } | |
| 265 | |||
| 266 | 210033 | void CommandEncoder::Update(BufferRange dst, const void* data) { | |
| 267 | 210033 | Require(dst.GetBuffer(), "Update"); | |
| 268 | 210033 | RequireWords(dst, "Update"); | |
| 269 | 210033 | if (dst.Bytes() > kMaxUpdateBytes) { | |
| 270 | 1 | throw Error("Update of {} bytes of buffer \"{}\"; it writes at most {}; Queue::Write " | |
| 271 | "stages larger writes", | ||
| 272 | 1 | dst.Bytes(), dst.GetBuffer().Label(), kMaxUpdateBytes); | |
| 273 | } | ||
| 274 | 210032 | if (dst.Bytes() > 0) { | |
| 275 | 210032 | Record(UpdateCommand{&dst.GetBuffer(), dst.Offset(), dst.Bytes(), | |
| 276 | 210032 | Store(data, dst.Bytes(), 4)}); | |
| 277 | } | ||
| 278 | 210032 | } | |
| 279 | |||
| 280 | ✗ | void CommandEncoder::BuildAccel(Accel& dst, const AccelInput& input, BufferRange scratch, | |
| 281 | bool update) { | ||
| 282 | ✗ | Require(dst, "BuildAccel"); | |
| 283 | ✗ | Require(scratch.GetBuffer(), "BuildAccel"); | |
| 284 | ✗ | ForEachBuffer(input, [&](BufferRange range) { | |
| 285 | ✗ | Require(range.GetBuffer(), "BuildAccel"); | |
| 286 | ✗ | if (!Has(range.GetBuffer().Usage(), BufferUsage::AccelBuildInput)) { | |
| 287 | ✗ | throw Error("BuildAccel of \"{}\" reads buffer \"{}\", which lacks " | |
| 288 | "BufferUsage::AccelBuildInput", | ||
| 289 | ✗ | dst.Label(), range.GetBuffer().Label()); | |
| 290 | } | ||
| 291 | ✗ | }); | |
| 292 | ✗ | if (GetLevel(input) != dst.Level()) { | |
| 293 | ✗ | const bool top = dst.Level() == AccelLevel::Top; | |
| 294 | ✗ | throw Error("BuildAccel of {} \"{}\" of {}", top ? "TLAS" : "BLAS", dst.Label(), | |
| 295 | ✗ | top ? "geometry" : "instances"); | |
| 296 | } | ||
| 297 | ✗ | if (update && !Refittable(input)) { | |
| 298 | ✗ | throw Error("BuildAccel updating BLAS \"{}\", which wasn't made refittable", dst.Label()); | |
| 299 | } | ||
| 300 | |||
| 301 | ✗ | const AccelSizes sizes = m_recording.m_queue->GetDevice().AccelBuildSizes(input); | |
| 302 | ✗ | const uint64_t needed = update ? sizes.updateScratch : sizes.scratch; | |
| 303 | ✗ | if (!Has(scratch.GetBuffer().Usage(), BufferUsage::AccelStorage) || scratch.Bytes() < needed) { | |
| 304 | ✗ | throw Error( | |
| 305 | "BuildAccel of \"{}\" with {} bytes of scratch in buffer \"{}\"; it takes {} of " | ||
| 306 | "BufferUsage::AccelStorage", | ||
| 307 | ✗ | dst.Label(), scratch.Bytes(), scratch.GetBuffer().Label(), needed); | |
| 308 | } | ||
| 309 | ✗ | if (dst.Storage().Bytes() < sizes.storage) { | |
| 310 | ✗ | throw Error("BuildAccel of \"{}\", of {} bytes, of an input that takes {}", dst.Label(), | |
| 311 | ✗ | dst.Storage().Bytes(), sizes.storage); | |
| 312 | } | ||
| 313 | |||
| 314 | ✗ | Record(BuildAccelCommand{&dst, input, &scratch.GetBuffer(), scratch.Offset(), update}); | |
| 315 | ✗ | } | |
| 316 | |||
| 317 | 232582 | void CommandEncoder::Dispatch(Kernel& kernel, const void* params, size_t bytes, Size3 groups, | |
| 318 | std::optional<BufferRange> counts) { | ||
| 319 | 232582 | Require(kernel.GetProgram(), "Dispatch"); | |
| 320 | 232582 | if (bytes != kernel.ParamBytes()) { | |
| 321 | ✗ | throw Error("Dispatch of kernel {} with {} bytes of parameters; it takes {}", kernel.Name(), | |
| 322 | ✗ | bytes, kernel.ParamBytes()); | |
| 323 | } | ||
| 324 | 232582 | if (counts) { | |
| 325 | 77897 | Require(counts->GetBuffer(), "Dispatch"); | |
| 326 | 77897 | RequireCounts(kernel, *counts, groups); | |
| 327 | } else { | ||
| 328 | 154685 | groups = Grid(kernel, groups); | |
| 329 | } | ||
| 330 | |||
| 331 | 232582 | if (groups.x > 0 && groups.y > 0 && groups.z > 0) { | |
| 332 | 388376 | Record(DispatchCommand{.kernel = &kernel, | |
| 333 | .groups = groups, | ||
| 334 | 232582 | .data = Store(params, bytes, 8), | |
| 335 | .paramBytes = uint32_t(bytes), | ||
| 336 | 232582 | .counts = counts ? &counts->GetBuffer() : nullptr, | |
| 337 | 232582 | .countsOffset = counts ? counts->Offset() : 0}); | |
| 338 | } | ||
| 339 | 232582 | } | |
| 340 | |||
| 341 | 7028 | void CommandEncoder::Concurrent(bool concurrent) { | |
| 342 | 7028 | m_concurrent = concurrent; | |
| 343 | 7028 | m_barrier = true; | |
| 344 | 7028 | } | |
| 345 | |||
| 346 | 188585 | void CommandEncoder::Barrier() { | |
| 347 | 188585 | m_barrier = true; | |
| 348 | 188585 | } | |
| 349 | |||
| 350 | 49186 | void CommandEncoder::BeginZone(ZoneId zone) { | |
| 351 | 49186 | const auto index = uint32_t(m_recording.m_zones.size()); | |
| 352 | 49186 | m_recording.m_zones.push_back(zone); | |
| 353 | 49186 | m_recording.m_commands.push_back(ZoneCommand{index, true}); | |
| 354 | 49186 | m_open.push_back(index); | |
| 355 | 49186 | } | |
| 356 | |||
| 357 | 49186 | void CommandEncoder::EndZone() { | |
| 358 | 49186 | if (m_open.empty()) { | |
| 359 | ✗ | throw Error("EndZone with no zone open"); | |
| 360 | } | ||
| 361 | 49186 | m_recording.m_commands.push_back(ZoneCommand{m_open.back(), false}); | |
| 362 | 49186 | m_open.pop_back(); | |
| 363 | 49186 | } | |
| 364 | |||
| 365 | 146031 | CommandBuffer CommandEncoder::Finish(const FinishDesc& desc) { | |
| 366 | 146031 | if (!m_open.empty()) { | |
| 367 | ✗ | throw Error("Finish with zone \"{}\" still open", | |
| 368 | ✗ | m_recording.m_zones[m_open.back()].Name()); | |
| 369 | } | ||
| 370 | 146031 | m_concurrent = false; | |
| 371 | 146031 | m_barrier = false; | |
| 372 | |||
| 373 | 292062 | CommandBuffer finished = std::exchange(m_recording, CommandBuffer(*m_recording.m_queue, m_own)); | |
| 374 | 146031 | if (desc.reusable) { | |
| 375 | 2 | finished.m_reusable = true; | |
| 376 | 2 | finished.Close(); | |
| 377 | 2 | finished.HoldResources(); | |
| 378 | } | ||
| 379 | 146031 | return finished; | |
| 380 | ✗ | } | |
| 381 | |||
| 382 | 44741 | ZoneScope::~ZoneScope() { | |
| 383 | 44741 | try { | |
| 384 | 44741 | m_encoder.EndZone(); | |
| 385 | ✗ | } catch (const Error& error) { | |
| 386 | ✗ | Log(LogLevel::Error, "{}", error.what()); | |
| 387 | ✗ | } | |
| 388 | 44741 | } | |
| 389 | |||
| 390 | } | ||
| 391 |