Problems with shader on Android

I have a pixel shader that draws a nice anti aliased circle that works great on PC, but I’m having some problems on Android. I don’t understand what is happening and I hope someone here might be able to figure it out.

I have not had the opportunity to test this on a different device, so I don’t know if this is a general Android issue or something specific to my phone.

At first it appears to work as intended
Imgur

But if the radius is larger than 256 it draws a square instead
Imgur

If the radius is between 256 and 257 all the pixels outside the circle are semi-transparent, like the pixels on the edge of the circle are supposed to.
Imgur

This is the full code of my shader:

#if OPENGL
	#define VS_SHADERMODEL vs_3_0
	#define PS_SHADERMODEL ps_3_0
#else
	#define VS_SHADERMODEL vs_4_0
	#define PS_SHADERMODEL ps_4_0
#endif

Texture2D SpriteTexture;
float2 CircleCenter;
float CircleRadius;

sampler2D SpriteTextureSampler = sampler_state
{
	Texture = <SpriteTexture>;
};

struct VertexShaderOutput
{
	float4 Position : SV_POSITION;
	float4 Color : COLOR0;
	float2 TextureCoordinates : TEXCOORD0;
};

float4 CirclePS(VertexShaderOutput input) : COLOR
{
	float distanceToCenter = length(input.Position.xy - CircleCenter);
	float sdf = distanceToCenter - CircleRadius;
	
	float alpha = clamp(-sdf, 0, 1);
	float4 retval = tex2D(SpriteTextureSampler, input.TextureCoordinates) * input.Color;
	retval.a *= alpha;
	return retval;
}

float4 CircleOutlinePS(VertexShaderOutput input) : COLOR
{
	// Positioning of the outline. -1 for inside radius, 0 for around radius and 1 for outside radius.
	int linePosition = -1;
	float lineThickness = 2;
	float distanceToCenter = length(input.Position.xy - CircleCenter);
	float sdf = abs(distanceToCenter - (CircleRadius + lineThickness * linePosition)) - lineThickness / 2;
	
	float alpha = clamp(-sdf, 0, 1);
	float4 retval = tex2D(SpriteTextureSampler, input.TextureCoordinates) * input.Color;
	retval.a *= alpha;
	return retval;
}

technique Circle
{
	pass P0
	{
		PixelShader = compile PS_SHADERMODEL CirclePS();
	}
};

technique CircleOutline
{
	pass P0
	{
		PixelShader = compile PS_SHADERMODEL CircleOutlinePS();
	}
};

I am using my shader like this, where Pixel is a white 1x1 Texture2D and I believe the rest of the names are self explanatory.

void DrawCircle(Vector2 center, float radius, Color color)
{
    _spriteBatch.Begin(blendState: BlendState.NonPremultiplied, samplerState: SamplerState.PointClamp, effect: CircleShader, sortMode: SpriteSortMode.Immediate);
    CircleShader.CurrentTechnique = CircleShader.Techniques["Circle"];
    CircleShader.Parameters["CircleRadius"].SetValue(radius);
    CircleShader.Parameters["CircleCenter"].SetValue(FlippedY(center));
    Rectangle bounds = new Rectangle((int)(center.X - radius - 1), (int)(center.Y - radius + 1), (int)radius * 2 + 2, (int)radius * 2 + 2);
    _spriteBatch.Draw(Pixel, bounds, color);
    _spriteBatch.End();
}

The problem does not appear to be related to the amount of pixels drawn. I tried making the bounds the whole screen and it still broke at radius above 256, it just made the whole screen red instead of just the square around the circle.

You are rendering to a quad, to make the circle bigger, the quad also must grow. What I think I can see there is the uv values wrapping and eventually colouring the whole quad. Set the texture to clamp but you will still fill the quad if the circle is bigger than it.

I think…

I am drawing a quad of appropriate size for the size of circle I want. If I smoothly increase the circle radius it turns a solid color all at once, it is not being cut off at the sides.

This is almost certainly a precision problem. On your PC all calculations are done using 32 bit float/int. OpenGL ES has precision qualifiers (highp, mediump and lowp) for int and float, so calculations can be done at lower precision.

To find out which precision qualifiers MojoShader is using exactly, you could take a look at the generated GLSL code, which can be done in the debugger.

My guess is that this line causes an overflow:

float distanceToCenter = length(input.Position.xy - CircleCenter);

because the length function involves a square. Maybe try this, and see if it increases your range:

float distanceToCenter = length((input.Position.xy - CircleCenter) / 2) * 2;
2 Likes

And yes, other devices might not show the same problem, because a device is allowed to use higher precision, than the one specified.

Thanks, that made a difference!

Is there a way to set the precision or do I just have to work around whatever precision the device is using?

The precision is set in the GLSL shader code. MG uses MojoShader to generate GLSL from your HLSL source.

So if you want to change the precision, you either have to modify the MG shader compiler (MGFXC), or modify the GLSL code in your app, before it gets sent to OpenGL for compilation.

The generated GLSL code is a “readable” string, so both options should actually be pretty easy to do.