Thread safety comes down one simple rule: don’t access the same data from two threads if one is potentially changing it.
There a couple of ways to get around this, you can use e.g. a FThreadSafeBool to guard data while you are copying on the producer thread or perhaps block unsafe code in a FScopeLock.
Personally I dislike locks for producer/consumer architecture and prefer to use something akin to an off-thread Lambda’s e.g.
//copy some data on your producer thread
const uint64 SafeData = UnsafeData;
RunLambdaOnGameThread([SafeData] {
//consume data on the specified thread (in this instance game thread)
}
Only the render thread should copy pointer data to UTexture2D. So you can do something like the following
void EnqueueTextureUpdate(const UTexture2D* TextureToUpdate, const TextureData* data){
if (TextureToUpdate == nullptr)
return;
struct FUploadTextureContext
{
uint8* Buffer; // Render thread assumes ownership
uint32 BufferPitch;
FTexture2DResource* DestTextureResource;
FThreadSafeBool* readyBool;
} TextureContext =
{
(uint8*)data->pBits, //this is the source buffer pointer
(data->windowSize.X) * 4,
(FTexture2DResource*)TextureToUpdate->Resource,
&textureReady,
};
ENQUEUE_UNIQUE_RENDER_COMMAND_ONEPARAMETER(
UpdateFrameTexture,
FUploadTextureContext, Context, TextureContext,
{
//Ensure our texture is valid
if (Context.DestTextureResource == nullptr)
return;
const FUpdateTextureRegion2D UpdateRegion(
0, 0, // Dest X, Y
0, 0, // Source X, Y
Context.DestTextureResource->GetSizeX(), // Width
Context.DestTextureResource->GetSizeY()); // Height
RHIUpdateTexture2D(
Context.DestTextureResource->GetTexture2DRHI(), // Destination GPU texture
0, // Mip map index
UpdateRegion, // Update region
Context.BufferPitch, // Source buffer pitch
Context.Buffer); // Source buffer pointer
// Delete the buffer if you own the data, otherwise comment this out
delete] Context.Buffer;
}
);
return;
}
or you can lock the texture while you update its contents e.g. (from my leap plugin)
UTexture2D* PrivateLeapImage::Texture32FromLeapImage(int32 SrcWidth, int32 SrcHeight, uint8* imageBuffer)
{
// Lock the texture so it can be modified
if (imagePointer == NULL)
return NULL;
uint8* MipData = static_cast<uint8*>(imagePointer->PlatformData->Mips[0].BulkData.Lock(LOCK_READ_WRITE));
// Create base mip.
uint8* DestPtr = NULL;
const uint8* SrcPtr = NULL;
for (int32 y = 0; y<SrcHeight; y++)
{
DestPtr = &MipData(SrcHeight - 1 - y) * SrcWidth * sizeof(FColor)];
SrcPtr = const_cast<uint8*>(&imageBuffer(SrcHeight - 1 - y) * SrcWidth]);
for (int32 x = 0; x<SrcWidth; x++)
{
//Grayscale, copy to all channels
*DestPtr++ = *SrcPtr;
*DestPtr++ = *SrcPtr;
*DestPtr++ = *SrcPtr;
*DestPtr++ = 0xFF;
SrcPtr++;
}
}
// Unlock the texture
imagePointer->PlatformData->Mips[0].BulkData.Unlock();
imagePointer->UpdateResource();
return imagePointer;
}
From my experience only ENQUEUE_UNIQUE_RENDER_COMMAND_ONEPARAMETER option gives VR level real-time results for larger images.