Threaded rendering issue

Hello,

I have an application where I have to send references to certain textures every frame. I have a crash issue though which I think is related to my code not being thread safe.

In my Tick Update I call a for loop for all the ten sender objects I use and I send a reference to certain textures to an external api. This usually works during runtime but it crashes on startup.

I think this happens because I am sending the reference for these textures on the game thread and it is not syncing correctly with the rendering thread.

I tried to use the ENQUEUE_UNIQUE_RENDER_COMMAND_XXXPARAMETER macro to enclose the code that sends the reference but is doesn’t seem to work. I also tried using the BeginFence command without much success either.

Unfortunately I couldn’t find much information about how to use these two techniques or any other technique to make my code thread safe.

Do you have any suggestions in regards to that? Are there any examples of a proper use of the ENQUEUE_UNIQUE_RENDER_COMMAND_XXXPARAMETER or BeginFence() and when it should be used? Are there any ways to test my code for thread safety?

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.

Thanks for your answer getnamo! I will give it and try and see if it works.

Out of curiosity in this page:

FRenderCommandFence::BeginFence is also mentioned but I couldn’t find any examples of how to use it.

Do you have any experience in using it? How much is it helping in terms of thread safety?

If I have a data buffer that I want to update the texture with every Tick(), from e.g. an Actor, is it safe to keep ONE persistent source buffer only, and in Tick() update its data and invoke EnqueueTextureUpdate() with the buffer as input (assuming the delete in the end of EnqueueTextureUpdate is removed)?

I am concerned that there might be a possibility where the game thread will reach the next Tick() before the render thread texture update command has been executed? Or is it guaranteed that when the next Tick() happens the render thread texture update issued last Tick() will already have been executed?