CARLA
Loading...
Searching...
No Matches
ImageUtil.cpp
Go to the documentation of this file.
1// Copyright (c) 2026 Computer Vision Center (CVC) at the Universitat Autonoma
2// de Barcelona (UAB).
3//
4// This work is licensed under the terms of the MIT license.
5// For a copy, see <https://opensource.org/licenses/MIT>.
6
9#include <Carla/Carla.h>
10
11#include <util/ue-header-guard-begin.h>
12#include <RHISurfaceDataConversion.h>
13#include <ImageWriteQueue.h>
14#include <HighResScreenshot.h>
15#include <RHIGPUReadback.h>
16#include <Async/ParallelFor.h>
17#include <Misc/App.h>
18#include <util/ue-header-guard-end.h>
19
20#include <chrono>
21#include <thread>
22
23template <typename F>
25{
26 F fn;
27public:
28
29 constexpr ScopedCallback(F&& fn) : fn(fn) { }
30
32};
33
34
35
36namespace ImageUtil
37{
39 const void* PixelData,
40 int32 SourcePitch,
41 FIntPoint Extent,
42 EPixelFormat Format,
43 FReadSurfaceDataFlags Flags,
44 TArrayView<FLinearColor> Out)
45 {
46 SourcePitch *= GPixelFormats[Format].BlockBytes;
47 return ConvertRAWSurfaceDataToFLinearColor(
48 Format,
49 Extent.X,
50 Extent.Y,
51 (uint8*)PixelData,
52 SourcePitch,
53 Out.GetData(),
54 Flags);
55 }
56
57
58
60 const void* PixelData,
61 int32 SourcePitch,
62 FIntPoint Extent,
63 EPixelFormat Format,
64 FReadSurfaceDataFlags Flags,
65 TArrayView<FColor> Out)
66 {
67 SourcePitch *= GPixelFormats[Format].BlockBytes;
68 auto OutPixelCount = Extent.X * Extent.Y;
69 switch (Format)
70 {
71 case PF_G16:
72 case PF_R16_UINT:
73 case PF_R16_SINT:
74 // Shadow maps
75 ConvertRawR16DataToFColor(Extent.X, Extent.Y, (uint8*)PixelData, SourcePitch, Out.GetData());
76 break;
77 case PF_R8G8B8A8:
78 ConvertRawR8G8B8A8DataToFColor(Extent.X, Extent.Y, (uint8*)PixelData, SourcePitch, Out.GetData());
79 break;
80 case PF_B8G8R8A8:
81 ConvertRawB8G8R8A8DataToFColor(Extent.X, Extent.Y, (uint8*)PixelData, SourcePitch, Out.GetData());
82 break;
83 case PF_A2B10G10R10:
84 ConvertRawR10G10B10A2DataToFColor(Extent.X, Extent.Y, (uint8*)PixelData, SourcePitch, Out.GetData());
85 break;
86 case PF_FloatRGBA:
87 case PF_R16G16B16A16_UNORM:
88 case PF_R16G16B16A16_SNORM:
89 ConvertRawR16G16B16A16FDataToFColor(Extent.X, Extent.Y, (uint8*)PixelData, SourcePitch, Out.GetData(), Flags.GetLinearToGamma());
90 break;
91 case PF_FloatR11G11B10:
92 ConvertRawR11G11B10DataToFColor(Extent.X, Extent.Y, (uint8*)PixelData, SourcePitch, Out.GetData(), Flags.GetLinearToGamma());
93 break;
94 case PF_A32B32G32R32F:
95 ConvertRawR32G32B32A32DataToFColor(Extent.X, Extent.Y, (uint8*)PixelData, SourcePitch, Out.GetData(), Flags.GetLinearToGamma());
96 break;
97 case PF_A16B16G16R16:
98 ConvertRawR16G16B16A16DataToFColor(Extent.X, Extent.Y, (uint8*)PixelData, SourcePitch, Out.GetData());
99 break;
100 case PF_G16R16:
101 ConvertRawR16G16DataToFColor(Extent.X, Extent.Y, (uint8*)PixelData, SourcePitch, Out.GetData());
102 break;
103 case PF_DepthStencil: // Depth / Stencil
104 ConvertRawD32S8DataToFColor(Extent.X, Extent.Y, (uint8*)PixelData, SourcePitch, Out.GetData(), Flags);
105 break;
106 case PF_X24_G8: // Depth / Stencil
107 ConvertRawR24G8DataToFColor(Extent.X, Extent.Y, (uint8*)PixelData, SourcePitch, Out.GetData(), Flags);
108 break;
109 case PF_R32_FLOAT: // Depth
110 for (uint32 Y = 0; Y < (uint32)Extent.Y; Y++)
111 {
112 auto SrcPtr = (float*)((uint8*)PixelData + Y * SourcePitch);
113 auto DestPtr = Out.GetData() + Y * Extent.X;
114 for (uint32 X = 0; X < (uint32)Extent.X; X++)
115 {
116 *DestPtr = FLinearColor(SrcPtr[0], 0.f, 0.f, 1.f).QuantizeRound();
117 ++SrcPtr;
118 ++DestPtr;
119 }
120 }
121 break;
122 case PF_R16G16B16A16_UINT:
123 case PF_R16G16B16A16_SINT:
124 ConvertRawR16G16B16A16DataToFColor(Extent.X, Extent.Y, (uint8*)PixelData, SourcePitch, Out.GetData());
125 break;
126 case PF_G8:
127 ConvertRawR8DataToFColor(Extent.X, Extent.Y, (uint8*)PixelData, SourcePitch, Out.GetData());
128 break;
129 default:
130 UE_LOG(LogCarla, Warning, TEXT("Unsupported format %llu"), (unsigned long long)Format);
131 return false;
132 }
133 return true;
134 }
135
136
137
139 UTextureRenderTarget2D& RenderTarget,
140 TArray<FColor>& Out)
141 {
142 check(IsInGameThread());
143 auto Resource = RenderTarget.GameThread_GetRenderTargetResource();
144 if (Resource == nullptr)
145 {
146 return false;
147 }
148 FReadSurfaceDataFlags ReadFlags(RCM_UNorm);
149 ReadFlags.SetLinearToGamma(true);
150 return Resource->ReadPixels(Out, ReadFlags);
151 }
152
153
154
156 UTextureRenderTarget2D& RenderTarget,
157 TArray64<FColor>& Out)
158 {
159 auto Resource = RenderTarget.GameThread_GetRenderTargetResource();
160 if (Resource == nullptr)
161 {
162 return false;
163 }
164 FReadSurfaceDataFlags ReadFlags(RCM_UNorm);
165 ReadFlags.SetLinearToGamma(true);
166 Out.SetNum(RenderTarget.GetSurfaceWidth() * RenderTarget.GetSurfaceHeight());
167 return Resource->ReadPixelsPtr(Out.GetData(), ReadFlags);
168 }
169
170
171
172 TUniquePtr<TImagePixelData<FColor>> ReadImageData(
173 UTextureRenderTarget2D& RenderTarget)
174 {
175 const auto Size = FIntPoint(
176 RenderTarget.GetSurfaceWidth(),
177 RenderTarget.GetSurfaceHeight());
178 auto PixelData = MakeUnique<TImagePixelData<FColor>>(Size);
179 ReadImageData(RenderTarget, PixelData->Pixels);
180 return PixelData;
181 }
182
183
184
185 TFuture<bool> SaveImageData(
186 UTextureRenderTarget2D& RenderTarget,
187 const FStringView& Path)
188 {
189 return SaveImageData(ReadImageData(RenderTarget), Path);
190 }
191
192
193
194 TFuture<bool> SaveImageData(
195 TUniquePtr<TImagePixelData<FColor>> Data,
196 const FStringView& Path)
197 {
198 auto& HighResScreenshotConfig = GetHighResScreenshotConfig();
199 auto ImageTask = MakeUnique<FImageWriteTask>();
200 ImageTask->PixelData = MoveTemp(Data);
201 ImageTask->Filename = Path;
202 ImageTask->Format = EImageFormat::PNG;
203 ImageTask->CompressionQuality = (int32)EImageCompressionQuality::Default;
204 ImageTask->bOverwriteFile = true;
205 ImageTask->PixelPreProcessors.Add(TAsyncAlphaWrite<FColor>(255));
206 return HighResScreenshotConfig.ImageWriteQueue->Enqueue(MoveTemp(ImageTask));
207 }
208
209
210
212 {
213 EPixelFormat Format;
214 FIntPoint Size;
216 // Pool-owned readback: lifetime managed by Pool/SlotIndex pair. Pointer
217 // is non-owning when Pool is set, owning (via FallbackReadback) otherwise.
218 FRHIGPUTextureReadback* Readback = nullptr;
219 TUniquePtr<FRHIGPUTextureReadback> FallbackReadback;
221 int32 SlotIndex = INDEX_NONE;
222 };
223
226 UTextureRenderTarget2D& RenderTarget,
229 {
230 auto& CmdList = FRHICommandListImmediate::Get();
231 auto Resource = static_cast<FTextureRenderTarget2DResource*>(
232 RenderTarget.GetResource());
233 // No resource: the RHI never created it (-nullrhi) or it was released.
234 // Leaving Self.Readback null makes the caller drop this frame.
235 if (Resource == nullptr)
236 {
237 return;
238 }
239 auto Texture = Resource->GetRenderTargetTexture();
240 if (Texture == nullptr)
241 {
242 return;
243 }
244 Self.Callback = std::move(Callback);
245 Self.Pool = std::move(Pool);
246 if (Self.Pool)
247 {
248 Self.Readback = Self.Pool->Acquire(Self.SlotIndex);
249 }
250 if (Self.Readback == nullptr)
251 {
252 Self.FallbackReadback = MakeUnique<FRHIGPUTextureReadback>(
253 TEXT("ReadImageData-Readback"));
254 Self.Readback = Self.FallbackReadback.Get();
255 }
256 Self.Size = Texture->GetSizeXY();
257 Self.Format = Texture->GetFormat();
258 auto ResolveRect = FResolveRect();
259 Self.Readback->EnqueueCopy(CmdList, Texture, ResolveRect);
260
261 // Record the copy and return -- never flush or wait for GPU completion
262 // here, on the render thread, per camera.
263 //
264 // Non-blocking callers (path-traced rt_lens sensor) poll completion
265 // off-thread (ReadImageDataEndAsync); any synchronous wait/flush here can
266 // deadlock the whole server under load (proven with gdb: the render thread
267 // blocks in RHIGetRenderQueryResult(..., bWait=true) mid-render-command
268 // inside ProcessInterruptQueueUntil (VulkanQuery.cpp) so it can no longer
269 // drive GPU submission -- RHIThread/RHISubmission sit idle and the game
270 // thread freezes behind it at FFrameEndSync).
271 //
272 // Blocking callers (all raster cameras) are delivered by
273 // FlushBatchedReadbacks() at the end of the sensor tick: one GPU sync for
274 // the whole camera batch. The historical per-camera flush + query-wait
275 // right here drained the entire GPU pipeline once per camera per frame
276 // (~13 ms each, resolution-independent), serializing the render thread
277 // and starving the GPU (~58% SM utilization at 5 cameras).
278 }
279
280 static void ReadImageDataEnd(
282 {
283 int32 RowPitch, BufferHeight;
284 auto MappedPtr = Self.Readback->Lock(RowPitch, &BufferHeight);
285 if (MappedPtr != nullptr)
286 {
287 ScopedCallback Unlock = [&] { Self.Readback->Unlock(); };
288 Self.Callback(MappedPtr, RowPitch, BufferHeight, Self.Format, Self.Size);
289 }
290 if (Self.Pool && Self.SlotIndex != INDEX_NONE)
291 {
292 Self.Pool->Release(Self.SlotIndex);
293 Self.SlotIndex = INDEX_NONE;
294 }
295 }
296
299 {
300 // Poll on a BACKGROUND worker, never a named thread. ENamedThreads::
301 // HighTaskPriority can be serviced by the RHIThread, and a busy IsReady()
302 // poll there blocks GPU submission -- the copy then never completes (its
303 // fence never signals), occlusion/other RHI fences stall, and the whole
304 // server starves (proven via gdb: RHIThread stuck in this poll loop).
305 AsyncTask(
306 ENamedThreads::AnyBackgroundThreadNormalTask, [
307 Self = std::move(Self)]() mutable
308 {
309 // Poll for GPU copy completion off-thread (never blocks the render thread).
310 // Bounded: if the copy has not completed after a few seconds (e.g. the
311 // capture stopped, so its command buffer was never submitted) drop this
312 // frame and release any pool slot rather than leak a spinning task.
313 uint64 Spins = 0;
314 while (!Self.Readback->IsReady())
315 {
316 std::this_thread::sleep_for(std::chrono::microseconds(200));
317 if (++Spins > 25000u) // ~5 s
318 {
319 if (Self.Pool && Self.SlotIndex != INDEX_NONE)
320 {
321 Self.Pool->Release(Self.SlotIndex);
322 Self.SlotIndex = INDEX_NONE;
323 }
324 return;
325 }
326 }
327 // The copy is complete, but FRHIGPUTextureReadback::Lock() maps via the
328 // immediate command list and asserts IsInRenderingThread(); it cannot run
329 // on this worker thread. Hand the final Lock/decode/deliver to the render
330 // thread. Because the readback is already ready, this Lock is a cheap
331 // CPU-side map -- it does NOT wait on the GPU, so the render thread is not
332 // stalled and cannot deadlock.
333 ENQUEUE_RENDER_COMMAND(RtlReadbackDeliver)(
334 [Self = MoveTemp(Self)](FRHICommandListImmediate&) mutable
335 {
336 ReadImageDataEnd(Self);
337 });
338 });
339 }
340
341 // Blocking readbacks recorded this tick, waiting for the single per-tick
342 // GPU sync in FlushBatchedReadbacks(). Render-thread access only.
343 static TArray<ReadImageDataContext> GBatchedReadbacks;
344
346 {
347 check(IsInRenderingThread());
348 GBatchedReadbacks.Add(MoveTemp(Context));
349 }
350
352 {
353 ENQUEUE_RENDER_COMMAND(FlushBatchedReadbacksCmd)(
354 [](FRHICommandListImmediate& CmdList)
355 {
356 if (GBatchedReadbacks.IsEmpty())
357 return;
358 TArray<ReadImageDataContext> Batch = MoveTemp(GBatchedReadbacks);
359 GBatchedReadbacks.Reset();
360
361 // One GPU sync for the whole batch. Every camera's capture and copy is
362 // already recorded ahead of this point in render-command order, so a
363 // single flush + query-wait guarantees all of them completed -- the same
364 // per-tick "all sensors delivered" guarantee the old per-camera wait
365 // gave, at one pipeline drain per tick instead of one per camera.
366 {
367 TRACE_CPUPROFILER_EVENT_SCOPE_STR("FlushBatchedReadbacks Sync");
368 static thread_local auto RenderQueryPool =
369 RHICreateRenderQueryPool(RQT_AbsoluteTime);
370 auto Query = RenderQueryPool->AllocateQuery();
371 CmdList.EndRenderQuery(Query.GetQuery());
372 CmdList.ImmediateFlush(EImmediateFlushType::FlushRHIThread);
373 uint64 DeltaTime;
374 RHIGetRenderQueryResult(Query.GetQuery(), DeltaTime, true);
375 Query.ReleaseQuery();
376 }
377
378 // Lock() needs the render thread, but after the sync it is a cheap
379 // CPU-side map. Decode + delivery is per-sensor independent (each
380 // callback owns its pixel buffer and data stream), so it runs wide.
381 struct FMappedReadback
382 {
383 void* Data = nullptr;
384 int32 RowPitch = 0;
385 int32 BufferHeight = 0;
386 };
387 TArray<FMappedReadback> Mapped;
388 Mapped.SetNum(Batch.Num());
389 for (int32 Index = 0; Index < Batch.Num(); ++Index)
390 {
391 Mapped[Index].Data = Batch[Index].Readback->Lock(
392 Mapped[Index].RowPitch, &Mapped[Index].BufferHeight);
393 }
394 {
395 TRACE_CPUPROFILER_EVENT_SCOPE_STR("FlushBatchedReadbacks Deliver");
396 ParallelFor(Batch.Num(), [&](int32 Index)
397 {
398 if (Mapped[Index].Data != nullptr)
399 {
400 Batch[Index].Callback(
401 Mapped[Index].Data,
402 Mapped[Index].RowPitch,
403 Mapped[Index].BufferHeight,
404 Batch[Index].Format,
405 Batch[Index].Size);
406 }
407 });
408 }
409 for (int32 Index = 0; Index < Batch.Num(); ++Index)
410 {
411 if (Mapped[Index].Data != nullptr)
412 Batch[Index].Readback->Unlock();
413 if (Batch[Index].Pool && Batch[Index].SlotIndex != INDEX_NONE)
414 Batch[Index].Pool->Release(Batch[Index].SlotIndex);
415 }
416 });
417 }
418
420 UTextureRenderTarget2D& RenderTarget,
423 bool bNonBlocking)
424 {
425 if (!FApp::CanEverRender())
426 {
427 return false;
428 }
429 if (IsInRenderingThread())
430 {
431 ReadImageDataContext Context = { };
432 ReadImageDataBegin(Context, RenderTarget, std::move(Pool), std::move(Callback));
433 if (Context.Readback == nullptr)
434 return false;
435 // Non-blocking variant (rt_lens): completion polled off-thread.
436 // Blocking variant (raster cameras): delivered by the per-tick batch.
437 if (bNonBlocking)
438 ReadImageDataEndAsync(std::move(Context));
439 else
440 ReadImageDataAddToBatch(std::move(Context));
441 }
442 else
443 {
444 ENQUEUE_RENDER_COMMAND(ReadImageDataAsyncCmd)([
445 &RenderTarget, Pool = std::move(Pool), Callback = std::move(Callback), bNonBlocking
446 ](auto& CmdList) mutable
447 {
448 ReadImageDataContext Context = { };
449 ReadImageDataBegin(Context, RenderTarget, std::move(Pool), std::move(Callback));
450 if (Context.Readback == nullptr)
451 return;
452 if (bNonBlocking)
453 ReadImageDataEndAsync(std::move(Context));
454 else
455 ReadImageDataAddToBatch(std::move(Context));
456 });
457
458 }
459 return true;
460 }
461
462
463
465 UTextureRenderTarget2D& RenderTarget,
467 {
468 return ReadImageDataAsync(RenderTarget, nullptr, std::move(Callback), false);
469 }
470
471
472
474 AShaderBasedSensor& Sensor,
476 {
477 auto RenderTarget = Sensor.GetCaptureRenderTarget();
478 if (RenderTarget == nullptr)
479 return false;
480 return ReadImageDataAsync(
481 *RenderTarget,
482 Sensor.GetReadbackPool(),
483 std::move(Callback));
484 }
485
486
487
489 UTextureRenderTarget2D& RenderTarget,
491 bool bNonBlocking,
493 {
494 return ReadImageDataAsync(RenderTarget, std::move(Pool), [Callback = std::move(Callback)](
495 const void* Mapping,
496 size_t RowPitch,
497 size_t BufferHeight,
498 EPixelFormat Format,
499 FIntPoint Size) -> bool
500 {
501 FReadSurfaceDataFlags Flags;
502 TArray<FColor> Pixels;
503 Pixels.SetNum(Size.X * Size.Y);
504 if (!DecodePixelsByFormat(Mapping, RowPitch, Size, Format, Flags, Pixels))
505 return false;
506 return Callback(Pixels, Size);
507 }, bNonBlocking);
508 }
509
510
511
513 AShaderBasedSensor& Sensor,
515 {
516 auto RenderTarget = Sensor.GetCaptureRenderTarget();
517 if (RenderTarget == nullptr)
518 return false;
519 // Pass the sensor's readback pool, as ReadSensorImageDataAsync does:
520 // without it every camera allocated a fresh staging buffer (8 MB at
521 // 1080p) on every frame.
523 *RenderTarget,
524 std::move(Callback),
525 /*bNonBlocking=*/false,
526 Sensor.GetReadbackPool());
527 }
528
529
530
532 UTextureRenderTarget2D& RenderTarget,
534 bool bNonBlocking,
536 {
537 return ReadImageDataAsync(RenderTarget, std::move(Pool), [Callback = std::move(Callback)](
538 const void* Mapping,
539 size_t RowPitch,
540 size_t BufferHeight,
541 EPixelFormat Format,
542 FIntPoint Size) -> bool
543 {
544 FReadSurfaceDataFlags Flags;
545 TArray<FLinearColor> Pixels;
546 Pixels.SetNum(Size.X * Size.Y);
547 if (!DecodePixelsByFormat(Mapping, RowPitch, Size, Format, Flags, Pixels))
548 return false;
549 return Callback(Pixels, Size);
550 }, bNonBlocking);
551 }
552
553
554
556 AShaderBasedSensor& Sensor,
558 {
559 auto RenderTarget = Sensor.GetCaptureRenderTarget();
560 if (RenderTarget == nullptr)
561 return false;
562 // Recycle the staging buffer through the sensor's pool, as the FColor
563 // variant does.
565 *RenderTarget,
566 std::move(Callback),
567 /*bNonBlocking=*/false,
568 Sensor.GetReadbackPool());
569 }
570}
TSharedPtr< FRHIGPUReadbackPool, ESPMode::ThreadSafe > FRHIGPUReadbackPoolPtr
UTextureRenderTarget2D * GetCaptureRenderTarget()
FRHIGPUReadbackPoolPtr GetReadbackPool() const
Per-sensor pool of recyclable RHI readback objects.
A sensor that produces data by applying post-process materials (shaders) to a scene capture image.
constexpr ScopedCallback(F &&fn)
Definition ImageUtil.cpp:29
void FlushBatchedReadbacks()
static TArray< ReadImageDataContext > GBatchedReadbacks
bool DecodePixelsByFormat(const void *PixelData, int32 SourcePitch, FIntPoint Extent, EPixelFormat Format, FReadSurfaceDataFlags Flags, TArrayView< FLinearColor > Out)
Definition ImageUtil.cpp:38
bool ReadImageDataAsync(UTextureRenderTarget2D &RenderTarget, FRHIGPUReadbackPoolPtr Pool, ReadImageDataAsyncCallback &&Callback, bool bNonBlocking)
std::function< bool(const void *, size_t, size_t, EPixelFormat, FIntPoint) > ReadImageDataAsyncCallback
Definition ImageUtil.h:34
std::function< bool(TArrayView< const FLinearColor >, FIntPoint) > ReadImageDataAsyncCallbackFLinearColor
Definition ImageUtil.h:54
std::function< bool(TArrayView< const FColor >, FIntPoint) > ReadImageDataAsyncCallbackFColor
Definition ImageUtil.h:44
static void ReadImageDataEndAsync(ReadImageDataContext &&Self)
bool ReadSensorImageDataAsyncFColor(AShaderBasedSensor &Sensor, ReadImageDataAsyncCallbackFColor &&Callback)
static void ReadImageDataAddToBatch(ReadImageDataContext &&Context)
static void ReadImageDataEnd(ReadImageDataContext &Self)
bool ReadImageDataAsyncFColor(UTextureRenderTarget2D &RenderTarget, ReadImageDataAsyncCallbackFColor &&Callback, bool bNonBlocking, FRHIGPUReadbackPoolPtr Pool)
bool ReadSensorImageDataAsync(AShaderBasedSensor &Sensor, ReadImageDataAsyncCallback &&Callback)
static void ReadImageDataBegin(ReadImageDataContext &Self, UTextureRenderTarget2D &RenderTarget, FRHIGPUReadbackPoolPtr Pool, ReadImageDataAsyncCallback &&Callback)
TFuture< bool > SaveImageData(UTextureRenderTarget2D &RenderTarget, const FStringView &Path)
bool ReadImageDataAsyncFLinearColor(UTextureRenderTarget2D &RenderTarget, ReadImageDataAsyncCallbackFLinearColor &&Callback, bool bNonBlocking, FRHIGPUReadbackPoolPtr Pool)
bool ReadImageData(UTextureRenderTarget2D &RenderTarget, TArray< FColor > &Out)
bool ReadSensorImageDataAsyncFLinearColor(AShaderBasedSensor &Sensor, ReadImageDataAsyncCallbackFLinearColor &&Callback)
TUniquePtr< FRHIGPUTextureReadback > FallbackReadback
ReadImageDataAsyncCallback Callback
FRHIGPUReadbackPoolPtr Pool
FRHIGPUTextureReadback * Readback