FSlateDrawElement::MakeCustom bug?

In short: even the most basic FSlateDrawElement::MakeCustom call is not working (e.g. the DrawRenderThread is never called) using Unreal Engine 4.7.5.

How to reproduce:

  • create a class deriving from ICustomSlateElement

  • override a SLeafWidget

  • override OnPaint to call FSlateDrawElement::MakeCustom using the ICustomSlateElement class instance

  • (bug??) DrawRenderThread is never called.

See source code (where class SRPMDialWidget : public SLeafWidget)

class FDialDrawer : public ICustomSlateElement
{
public:
	FDialDrawer() {}
	~FDialDrawer() {}
private:
	/**
	* ICustomSlateElement interface
	*/
	virtual void DrawRenderThread(FRHICommandListImmediate& RHICmdList, const void* InWindowBackBuffer) override
	{
		UE_LOG(LogTemp, Log, TEXT("FDialDrawer::DrawRenderThread ***this is never called***"));
	}
};

SRPMDialWidget::SRPMDialWidget()
	: Drawer(new FDialDrawer()) // of type TSharedPtr<class FDialDrawer, ESPMode::ThreadSafe>
{
}

BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SRPMDialWidget::Construct(const FArguments& InArgs)
{
	OwnerHUD = InArgs._OwnerHUD;
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION

int32 SRPMDialWidget::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyClippingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const
{
	if (OwnerHUD.IsValid() == true)
	{
		uint32 TopLayer = LayerId + 1;
		FSlateDrawElement::MakeCustom(OutDrawElements, TopLayer, Drawer);
	}
	return LayerId;
}

FVector2D SRPMDialWidget::ComputeDesiredSize() const
{
	static const FVector2D desiredSize(64.0f, 64.0f);

	return desiredSize;
}

Are you testing it in PIE or standalone? I’m having the exact same issue, but only when testing inside PIE.

indeed - it does work but only in the *maximized editor viewport or running standalone.

I can now mix Slate UI elements & positioning with custom drawn items!

Glad you got it working! :slight_smile: I’ve been trying to get custom elements to draw in the editor viewport for quite some time now too, but the maximized editor viewport trick doesn’t seem to work for me.

Can you please share some info about your editor/custom element setup? Do you render your custom elements using a Canvas?

From my tests, it seems the custom element is never rendered because it gets killed in the FSlateElementBatcher::AddElements function, where it gets “scissored” out at line 77 (ElementBatcher.cpp).

BTW I’m going to post a new question in the Bugs section, as I think it really is a bug after all.

I have a ‘FCanvasDrawer’ abstract class that takes care of pushing canvas size to the rendertarget (the class being a shameless copy of one of the UDK classes!)

Warning notice: I am not sure this is actually worth the effort. The rendering quality of FCanvas is really low, lines and polygons are not antialiased making the whole solution a bit useless to draw a HUD!!

#pragma once

typedef TSharedPtr<class FCanvasDrawer, ESPMode::ThreadSafe> FThreadSafeFCanvasDrawerPtr;

/**
 * 
 */
class FCanvasDrawer : public ICustomSlateElement
{
public:
	FCanvasDrawer();
	~FCanvasDrawer();

	/** Debug - Frame counter */
	int Frames;

	/**
	* Sets up the canvas for rendering
	*/
	bool BeginRenderingCanvas(const FIntRect& InCanvasRect, const FIntRect& InClippingRect, bool bInIsRealtime);

	/**
	* Delegates rendering method
	*/
	virtual void Draw(FCanvas& Canvas, const FVector2D& Size) = 0;

private:
	/**
	* ICustomSlateElement interface
	*/
	virtual void DrawRenderThread(FRHICommandListImmediate& RHICmdList, const void* InWindowBackBuffer) override;

private:
	/** Render target that the canvas renders to */
	class FSlateCanvasRenderTarget* RenderTarget;
	/** Whether preview is using realtime values */
	bool bIsRealtime;
};

CPP file:

#include "FCanvasDrawer.h"

/**
* Simple representation of the backbuffer
* This class may only be accessed from the render thread
*/
class FSlateCanvasRenderTarget : public FRenderTarget
{
public:
	/** FRenderTarget interface */
	virtual FIntPoint GetSizeXY() const
	{
		return ClippingRect.Size();
	}

	/** Sets the texture that this target renders to */
	void SetRenderTargetTexture(FTexture2DRHIRef& InRHIRef)
	{
		RenderTargetTextureRHI = InRHIRef;
	}

	/** Clears the render target texture */
	void ClearRenderTargetTexture()
	{
		RenderTargetTextureRHI.SafeRelease();
	}

	/** Sets the viewport rect for the render target */
	void SetViewRect(const FIntRect& InViewRect)
	{
		ViewRect = InViewRect;
	}

	/** Gets the viewport rect for the render target */
	const FIntRect& GetViewRect() const
	{
		return ViewRect;
	}

	/** Sets the clipping rect for the render target */
	void SetClippingRect(const FIntRect& InClippingRect)
	{
		ClippingRect = InClippingRect;
	}

	/** Gets the clipping rect for the render target */
	const FIntRect& GetClippingRect() const
	{
		return ClippingRect;
	}
private:
	FIntRect ViewRect;
	FIntRect ClippingRect;
};

/* --------------------FCanvasDrawer-------------------------- */
FCanvasDrawer::FCanvasDrawer()
	: RenderTarget(new FSlateCanvasRenderTarget)
	, bIsRealtime(false)
	, Frames(0)
{
}

FCanvasDrawer::~FCanvasDrawer()
{
	delete RenderTarget;
}

bool FCanvasDrawer::BeginRenderingCanvas(const FIntRect& InCanvasRect, const FIntRect& InClippingRect, bool bInIsRealtime)
{
	if (InCanvasRect.Size().X > 0 && InCanvasRect.Size().Y > 0 && InClippingRect.Size().X > 0 && InClippingRect.Size().Y > 0)
	{
		/**
		* Struct to contain all info that needs to be passed to the render thread
		*/
		struct FRenderInfo
		{
			/** Size of the Canvas tile */
			FIntRect CanvasRect;
			/** How to clip the canvas tile */
			FIntRect ClippingRect;
			/** Whether preview is using realtime values */
			bool bIsRealtime;
		};

		FRenderInfo RenderInfo;
		RenderInfo.CanvasRect = InCanvasRect;
		RenderInfo.ClippingRect = InClippingRect;
		RenderInfo.bIsRealtime = bInIsRealtime;

		ENQUEUE_UNIQUE_RENDER_COMMAND_TWOPARAMETER
			(
			BeginRenderingSlateCanvas,
			FCanvasDrawer*, CanvasDrawer, this,
			FRenderInfo, InRenderInfo, RenderInfo,
			{
				CanvasDrawer->RenderTarget->SetViewRect(InRenderInfo.CanvasRect);
				CanvasDrawer->RenderTarget->SetClippingRect(InRenderInfo.ClippingRect);
				CanvasDrawer->bIsRealtime = InRenderInfo.bIsRealtime;
			}
		);

		return true;
	}

	return false;
}

void FCanvasDrawer::DrawRenderThread(FRHICommandListImmediate& RHICmdList, const void* InWindowBackBuffer)
{
	Frames++;

	// Clip the canvas to avoid having to set UV values
	FIntRect ClippingRect = RenderTarget->GetClippingRect();

	RHICmdList.SetScissorRect(true,
		ClippingRect.Min.X,
		ClippingRect.Min.Y,
		ClippingRect.Max.X,
		ClippingRect.Max.Y);
	RenderTarget->SetRenderTargetTexture(*(FTexture2DRHIRef*)InWindowBackBuffer);
	{
		// Check realtime mode for whether to pass current time to canvas
		float CurrentTime = bIsRealtime ? (FApp::GetCurrentTime() - GStartTime) : 0.0f;
		float DeltaTime = bIsRealtime ? FApp::GetDeltaTime() : 0.0f;

		FCanvas Canvas(RenderTarget, NULL, CurrentTime, CurrentTime, DeltaTime, GMaxRHIFeatureLevel);
		{
			Canvas.SetAllowedModes(0);
			Canvas.SetRenderTargetRect(RenderTarget->GetViewRect());
			
			// Delegate drawing to child classes...
			Draw(Canvas, RenderTarget->GetSizeXY());
		}
		Canvas.Flush_RenderThread(RHICmdList, true);
	}
	RenderTarget->ClearRenderTargetTexture();
	RHICmdList.SetScissorRect(false, 0, 0, 0, 0);
}

Once you’ve got that, create a inherited class to actually draw something (in my case, a radar):

class FRadarDrawer : public FCanvasDrawer
{
public:
	FRadarDrawer() {}
	~FRadarDrawer() {}

private:
	void DrawCircle(FCanvas& Canvas, const FVector2D& Position, const float& Radius)
	{
		const int N = 24;
		float angleInc = PI * 2 / (float)N;
		float angle = 0;
		for (int i = 0; i < N; i++)
		{
			FVector2D start(Radius * cos(angle), Radius * sin(angle));
			FVector2D end(Radius * cos(angle + angleInc), Radius * sin(angle + angleInc));
			FCanvasLineItem lineItem(Position + start, Position + end);
			lineItem.LineThickness = 1;
			lineItem.SetColor(FLinearColor::Green);
			Canvas.DrawItem(lineItem);
			angle += angleInc;
		}
	}

	/**
	* ICustomSlateElement interface
	*/
	virtual void Draw(FCanvas& Canvas, const FVector2D& Size) override
	{
		if (Style != NULL)
		{
			const FVector2D Center(Size.X * 0.5f, Size.Y * 0.5f);
			DrawCircle(Canvas, Center, 0.5f * Size.X);
			DrawCircle(Canvas, Center, 0.4f * Size.X);
			DrawCircle(Canvas, Center, 0.3f * Size.X);
			DrawCircle(Canvas, Center, 0.2f * Size.X);
		}
	}
};

Drawing is done, now we need to include that ‘radar’ in a Slate element to get nice UI layout management done for us!

#pragma once

typedef TSharedPtr<class FRadarDrawer, ESPMode::ThreadSafe> FThreadSafeFRadarDrawerPtr;

/**
 * 
 */
class SRadarWidget : public SLeafWidget
{
public:
	SRadarWidget();
	~SRadarWidget();

	SLATE_BEGIN_ARGS(SRadarWidget)
	{}
	SLATE_END_ARGS()

	/** Constructs this widget with InArgs */
	void Construct(const FArguments& InArgs);

private:
	virtual int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyClippingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override;
	virtual FVector2D ComputeDesiredSize() const override
	{
		return FVector2D(512,512);
	}

	FThreadSafeFRadarDrawerPtr Drawer;
};

and cpp:

#include "FRadarDrawer.h" // could even be inlined in this class...
#include "SRadarWidget.h"

SRadarWidget::SRadarWidget()
	: Drawer(new FRadarDrawer())
{
}

SRadarWidget::~SRadarWidget()
{
	// Pass the preview element to the render thread so that it's deleted after it's shown for the last time
	ENQUEUE_UNIQUE_RENDER_COMMAND_ONEPARAMETER
		(
		SafeDeletePreviewElement,
		FThreadSafeFCanvasDrawerPtr, DrawerPtr, Drawer,
		{
			DrawerPtr.Reset();
		}
	);
}

BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SRadarWidget::Construct(const FArguments& InArgs)
{
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION

int32 SRadarWidget::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyClippingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const
{
		FSlateRect SlateCanvasRect = AllottedGeometry.GetClippingRect();
		FSlateRect ClippedCanvasRect = SlateCanvasRect.IntersectionWith(MyClippingRect);

		FIntRect CanvasRect(
			FMath::TruncToInt(FMath::Max(0.0f, SlateCanvasRect.Left)),
			FMath::TruncToInt(FMath::Max(0.0f, SlateCanvasRect.Top)),
			FMath::TruncToInt(FMath::Max(0.0f, SlateCanvasRect.Right)),
			FMath::TruncToInt(FMath::Max(0.0f, SlateCanvasRect.Bottom)));

		FIntRect ClippingRect(
			FMath::TruncToInt(FMath::Max(0.0f, ClippedCanvasRect.Left)),
			FMath::TruncToInt(FMath::Max(0.0f, ClippedCanvasRect.Top)),
			FMath::TruncToInt(FMath::Max(0.0f, ClippedCanvasRect.Right)),
			FMath::TruncToInt(FMath::Max(0.0f, ClippedCanvasRect.Bottom)));

		if (Drawer->BeginRenderingCanvas(CanvasRect, ClippingRect, true))
		{
			// Draw above everything else (not needed - mostly for debugging)
			uint32 TopLayer = LayerId + 1;
			FSlateDrawElement::MakeCustom(OutDrawElements, TopLayer, Drawer);
		}
	}
	return LayerId;
}

see long answer below :slight_smile:

Thanks for taking the time to post your code! This is nearly identical to my implementation (I started with a shameless copy of the UE4 class too), so I’m out of ideas about why I’m not able to preview the custom elements in-editor. :confused:

BTW, about your aliasing issues, maybe you can try to avoid drawing circles and lines dynamically and use textures or materials instead. You can then scale and rotate them based on your needs by applying transforms either to the canvas (see FCanvas::PushRelativeTransform) or to the draw elements themselves. I’m using FCanvas to draw bitmap fonts, and the quality is actually fairly good (comparable to Slate TTF text) even with complex scaling/rotations applied.

This should be fixed for 4.9, here’s the change that needed to be made in the master branch. https://github.com/EpicGames/UnrealEngine/commit/ebb2a6e41a8a744e85559b89b61d8447a3e141bf