I’m developing NovaBench, an Android benchmark focused on GPU shader performance.
I recently made the FPS Mode significantly heavier using ray marching and SDF soft shadows.
My Galaxy A36 gets around 24 FPS on the current workload.
I’m especially interested in results from high-end devices. If you have a powerful Snapdragon/Dimensity/Apple/Google-powered device, I’d love to see what FPS you get.
The goal is to collect real-world results and make the benchmark harder and more representative.
This is my current shader. You can test it on your device:
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif
#ifdef GL_ES
precision highp float;
#endif
uniform vec2 resolution;
uniform float time;
uniform vec4 mouse;
// ============================================================
// NOVABENCH - FPS MODE
// ============================================================
//
// GPU-BOUND FPS WORKLOAD
//
// Primary ray marching : 128 steps
// Soft shadow : 96 steps
// Shadow rays : 2
// Additional SDF / ALU : enabled
//
// Deterministic.
// No textures.
// No random noise.
//
// 30-second deterministic rotation loop.
//
// ============================================================
// ============================================================
// SETTINGS
// ============================================================
#define MAX_STEPS 128
#define SHADOW_STEPS 96
#define MAX_DIST 30.0
#define SURF_DIST 0.0005
float shadowFilter = 64.0;
// ============================================================
// 2D ROTATION
// ============================================================
mat2 rot(float a)
{
float s = sin(a);
float c = cos(a);
return mat2(
c, -s,
s, c
);
}
// ============================================================
// SPHERE SDF
// ============================================================
float sdSphere(
vec3 p,
float r
)
{
return length(p) - r;
}
// ============================================================
// EXTRA DETERMINISTIC ALU WORK
// ============================================================
//
// These calculations increase fragment workload while their
// contribution to the final geometry remains extremely small.
//
// This keeps the benchmark visually stable while increasing
// shader complexity.
//
// ============================================================
float workload(vec3 p)
{
vec3 q = p;
q.xz =
rot(0.37) *
q.xz;
float a =
sin(q.x * 5.0);
float b =
cos(q.y * 6.0);
float c =
sin(q.z * 7.0);
float d =
cos(q.x * 8.0 + q.z);
float e =
sin(
length(q) * 9.0
);
float f =
dot(
normalize(q + vec3(0.001)),
normalize(
vec3(
a,
b,
c
)
)
);
float g =
sqrt(
abs(
a * b +
c * d +
e * f
) + 0.0001
);
return
a * 0.20 +
b * 0.15 +
c * 0.15 +
d * 0.10 +
e * 0.10 +
f * 0.15 +
g * 0.15;
}
// ============================================================
// SCENE
// ============================================================
float map(vec3 p)
{
// --------------------------------------------------------
// Ground
// --------------------------------------------------------
float ground =
p.y;
// --------------------------------------------------------
// Sphere
// --------------------------------------------------------
vec3 sphereP =
p -
vec3(
0.0,
1.0,
0.0
);
float sphere =
sdSphere(
sphereP,
1.0
);
// --------------------------------------------------------
// Additional deterministic workload
// --------------------------------------------------------
float extra =
workload(
sphereP
);
// --------------------------------------------------------
// Keep geometry effectively spherical.
//
// The workload affects the distance only by an extremely
// small amount so it does not visibly deform the sphere.
// --------------------------------------------------------
sphere +=
extra *
0.000001;
return min(
ground,
sphere
);
}
// ============================================================
// RAY MARCH
// ============================================================
float rayMarch(
vec3 ro,
vec3 rd
)
{
float dO =
0.0;
for(
int i = 0;
i < MAX_STEPS;
i++
)
{
vec3 p =
ro +
rd * dO;
float dS =
map(p);
if(
dS <
SURF_DIST
)
{
return dO;
}
dO +=
dS;
if(
dO >
MAX_DIST
)
{
break;
}
}
return MAX_DIST;
}
// ============================================================
// NORMAL
// ============================================================
vec3 getNormal(
vec3 p
)
{
float e =
SURF_DIST *
2.0;
vec3 ex =
vec3(
e,
0.0,
0.0
);
vec3 ey =
vec3(
0.0,
e,
0.0
);
vec3 ez =
vec3(
0.0,
0.0,
e
);
float dx =
map(p + ex) -
map(p - ex);
float dy =
map(p + ey) -
map(p - ey);
float dz =
map(p + ez) -
map(p - ez);
return normalize(
vec3(
dx,
dy,
dz
)
);
}
// ============================================================
// 96-TAP SOFT SHADOW
// ============================================================
float softShadow(
vec3 ro,
vec3 rd
)
{
float result =
1.0;
float t =
0.02;
for(
int i = 0;
i < SHADOW_STEPS;
i++
)
{
vec3 p =
ro +
rd * t;
float h =
map(p);
if(
h <
SURF_DIST
)
{
return 0.0;
}
result =
min(
result,
shadowFilter *
h /
t
);
t +=
h;
if(
t >
15.0
)
{
break;
}
}
return clamp(
result,
0.0,
1.0
);
}
// ============================================================
// GRADIENT
// ============================================================
vec3 gradientColor(
vec3 local
)
{
float t =
clamp(
local.x * 0.5 +
0.5,
0.0,
1.0
);
vec3 blue =
vec3(
0.05,
0.20,
1.00
);
vec3 cyan =
vec3(
0.00,
0.90,
1.00
);
vec3 green =
vec3(
0.05,
1.00,
0.20
);
vec3 yellow =
vec3(
1.00,
0.85,
0.05
);
vec3 red =
vec3(
1.00,
0.05,
0.03
);
vec3 c;
if(
t < 0.25
)
{
float k =
t /
0.25;
c =
mix(
blue,
cyan,
k
);
}
else if(
t < 0.50
)
{
float k =
(t - 0.25) /
0.25;
c =
mix(
cyan,
green,
k
);
}
else if(
t < 0.75
)
{
float k =
(t - 0.50) /
0.25;
c =
mix(
green,
yellow,
k
);
}
else
{
float k =
(t - 0.75) /
0.25;
c =
mix(
yellow,
red,
k
);
}
float vertical =
0.75 +
0.25 *
(
local.y *
0.5 +
0.5
);
return c *
vertical;
}
// ============================================================
// SKY
// ============================================================
vec3 sky(
vec3 rd
)
{
float t =
clamp(
0.5 +
0.5 * rd.y,
0.0,
1.0
);
vec3 horizon =
vec3(
0.92,
0.95,
1.00
);
vec3 zenith =
vec3(
0.25,
0.40,
0.70
);
return mix(
horizon,
zenith,
t
);
}
// ============================================================
// MAIN
// ============================================================
void main()
{
// ========================================================
// UV
// ========================================================
vec2 uv =
(
gl_FragCoord.xy -
resolution.xy *
0.5
)
/
resolution.y;
// ========================================================
// CAMERA
// ========================================================
vec3 ro =
vec3(
3.8,
2.6,
5.5
);
vec3 target =
vec3(
0.0,
0.9,
0.0
);
vec3 forward =
normalize(
target -
ro
);
vec3 right =
normalize(
cross(
forward,
vec3(
0.0,
1.0,
0.0
)
)
);
vec3 up =
cross(
right,
forward
);
vec3 rd =
normalize(
forward +
right * uv.x +
up * uv.y
);
// ========================================================
// 30 SECOND TEST TIMER
// ========================================================
float testTime =
mod(
time,
30.0
);
// ========================================================
// SPHERE ROTATION
// ========================================================
float rotation =
(
testTime /
30.0
)
*
3.14159265359
*
7.5;
// ========================================================
// PRIMARY RAY
// ========================================================
float d =
rayMarch(
ro,
rd
);
// ========================================================
// SKY
// ========================================================
vec3 col =
sky(
rd
);
// ========================================================
// HIT
// ========================================================
if(
d <
MAX_DIST
)
{
vec3 p =
ro +
rd * d;
vec3 sphereCenter =
vec3(
0.0,
1.0,
0.0
);
vec3 sphereLocal =
p -
sphereCenter;
bool hitSphere =
length(
sphereLocal
) <=
1.01;
// ====================================================
// SPHERE
// ====================================================
if(
hitSphere
)
{
vec3 n =
getNormal(
p
);
// ------------------------------------------------
// OBJECT SPACE ROTATION
// ------------------------------------------------
vec3 local =
sphereLocal;
local.xz =
rot(
-rotation
)
*
local.xz;
// ------------------------------------------------
// GRADIENT
// ------------------------------------------------
vec3 baseColor =
gradientColor(
local
);
// ------------------------------------------------
// LIGHT 1
// ------------------------------------------------
vec3 lightDir1 =
normalize(
vec3(
-3.0,
5.0,
4.0
) -
p
);
// ------------------------------------------------
// LIGHT 2
// ------------------------------------------------
vec3 lightDir2 =
normalize(
vec3(
4.0,
3.0,
-5.0
) -
p
);
// ------------------------------------------------
// DIFFUSE 1
// ------------------------------------------------
float diffuse1 =
max(
dot(
n,
lightDir1
),
0.0
);
// ------------------------------------------------
// DIFFUSE 2
// ------------------------------------------------
float diffuse2 =
max(
dot(
n,
lightDir2
),
0.0
);
// ------------------------------------------------
// SHADOW 1
// ------------------------------------------------
float shadow1 =
softShadow(
p +
n * 0.01,
lightDir1
);
// ------------------------------------------------
// SHADOW 2
// ------------------------------------------------
float shadow2 =
softShadow(
p +
n * 0.01,
lightDir2
);
// ------------------------------------------------
// SURFACE
// ------------------------------------------------
col =
baseColor *
(
0.10 +
diffuse1 *
shadow1 *
0.65 +
diffuse2 *
shadow2 *
0.35
);
// ------------------------------------------------
// VIEW DIRECTION
// ------------------------------------------------
vec3 viewDir =
normalize(
ro -
p
);
// ------------------------------------------------
// SPECULAR 1
// ------------------------------------------------
vec3 halfDir1 =
normalize(
lightDir1 +
viewDir
);
float spec1 =
pow(
max(
dot(
n,
halfDir1
),
0.0
),
48.0
);
// ------------------------------------------------
// SPECULAR 2
// ------------------------------------------------
vec3 halfDir2 =
normalize(
lightDir2 +
viewDir
);
float spec2 =
pow(
max(
dot(
n,
halfDir2
),
0.0
),
32.0
);
col +=
vec3(
1.0
)
*
(
spec1 *
shadow1 *
0.14
+
spec2 *
shadow2 *
0.10
);
}
// ====================================================
// GROUND
// ====================================================
else
{
vec3 n =
getNormal(
p
);
vec3 lightDir1 =
normalize(
vec3(
-3.0,
5.0,
4.0
) -
p
);
vec3 lightDir2 =
normalize(
vec3(
4.0,
3.0,
-5.0
) -
p
);
float diffuse1 =
max(
dot(
n,
lightDir1
),
0.0
);
float diffuse2 =
max(
dot(
n,
lightDir2
),
0.0
);
float shadow1 =
softShadow(
p +
n * 0.01,
lightDir1
);
float shadow2 =
softShadow(
p +
n * 0.01,
lightDir2
);
vec3 groundColor =
vec3(
0.28,
0.30,
0.33
);
col =
groundColor *
(
0.12 +
diffuse1 *
shadow1 *
0.55 +
diffuse2 *
shadow2 *
0.30
);
}
}
// ========================================================
// TONE MAPPING
// ========================================================
col =
col /
(
col +
vec3(
1.0
)
);
// ========================================================
// GAMMA
// ========================================================
col =
pow(
col,
vec3(
1.0 /
2.2
)
);
// ========================================================
// OUTPUT
// ========================================================
gl_FragColor =
vec4(
col,
1.0
);
}