Added support for C# benchmarks (#46)

This commit is contained in:
Helehex
2024-02-09 07:43:04 -06:00
committed by GitHub
parent 8a3cda5617
commit 93bc7cd0d6
22 changed files with 742 additions and 84 deletions

9
Benchmark.cs Normal file
View File

@@ -0,0 +1,9 @@
using Godot;
public partial class Benchmark : RefCounted
{
public bool test_render_cpu = false;
public bool test_render_gpu = false;
public bool test_idle = false;
public bool test_physics = false;
}

View File

@@ -5,7 +5,8 @@ Thank you for your interest in contributing!
> **Note**
>
> This project only supports Godot's `master` branch (4.0's development branch),
> not Godot 3.x. Attempting to open this project in Godot 3.x will result in errors.
> not Godot 3.x.
> Attempting to open this project in Godot 3.x will result in errors.
## Adding new benchmarks
@@ -23,14 +24,15 @@ you can also create a new folder in the repository's root folder.
In the `_init()` function of your script, configure your benchmark by
initializing any desired `Benchmark` variables to true:
- **test_render_cpu:** Enable this for rendering benchmarks.
- `test_render_cpu`: Enable this for rendering benchmarks.
Leave it disabled for other benchmarks.
- **test_render_gpu:** Enable this for rendering benchmarks.
- `test_render_gpu`: Enable this for rendering benchmarks.
Leave it disabled for other benchmarks.
- **test_idle:** Enable this for non-rendering CPU-intensive benchmarks.
- `test_idle`: Enable this for non-rendering CPU-intensive benchmarks.
Leave it disabled for other benchmarks.
- **test_physics:** Enable this for physics benchmarks.
- `test_physics`: Enable this for physics benchmarks.
Leave it disabled for other benchmarks.
- > Leaving all of these disabled will only time the initial call to your `benchmark_` function (see below).
### Implement the benchmark
@@ -57,6 +59,9 @@ Remember to follow the
when writing new scripts. Adding type hints is recommended whenever possible,
unless you are specifically benchmarking non-typed scripts.
> C# benchmark functions must begin with `Benchmark`, instead of `benchmark_`.
> For C# benchmarks to be available, you must use the .NET version of the engine.
### Test the benchmark
1. Run the project in the editor.

9
Godot Benchmarks.csproj Normal file
View File

@@ -0,0 +1,9 @@
<Project Sdk="Godot.NET.Sdk/4.3.0-dev.2">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework Condition=" '$(GodotTargetPlatform)' == 'android' ">net7.0</TargetFramework>
<TargetFramework Condition=" '$(GodotTargetPlatform)' == 'ios' ">net8.0</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading>
<RootNamespace>GodotBenchmarks</RootNamespace>
</PropertyGroup>
</Project>

19
Godot Benchmarks.sln Normal file
View File

@@ -0,0 +1,19 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2012
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot Benchmarks", "Godot Benchmarks.csproj", "{A7FD1DC6-AE2B-4F17-B4BA-2FD940E01B47}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
ExportDebug|Any CPU = ExportDebug|Any CPU
ExportRelease|Any CPU = ExportRelease|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A7FD1DC6-AE2B-4F17-B4BA-2FD940E01B47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A7FD1DC6-AE2B-4F17-B4BA-2FD940E01B47}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A7FD1DC6-AE2B-4F17-B4BA-2FD940E01B47}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU
{A7FD1DC6-AE2B-4F17-B4BA-2FD940E01B47}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU
{A7FD1DC6-AE2B-4F17-B4BA-2FD940E01B47}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU
{A7FD1DC6-AE2B-4F17-B4BA-2FD940E01B47}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU
EndGlobalSection
EndGlobal

View File

@@ -1,92 +1,76 @@
extends Benchmark
const ITERATIONS = 10_000_000
const RANDOM_SEED = preload("res://main.gd").RANDOM_SEED
var rng := RandomNumberGenerator.new()
func benchmark_global_scope_randi() -> void:
# Reset the random seed to improve reproducibility of this benchmark.
seed(RANDOM_SEED)
seed(Manager.RANDOM_SEED)
for i in ITERATIONS:
randi()
# Reset the random seed again to improve reproducibility of other benchmarks.
seed(RANDOM_SEED)
func benchmark_randi() -> void:
rng.seed = RANDOM_SEED
rng.seed = Manager.RANDOM_SEED
for i in ITERATIONS:
rng.randi()
rng.seed = RANDOM_SEED
func benchmark_global_scope_randf() -> void:
seed(RANDOM_SEED)
seed(Manager.RANDOM_SEED)
for i in ITERATIONS:
randf()
seed(RANDOM_SEED)
func benchmark_randf() -> void:
rng.seed = RANDOM_SEED
rng.seed = Manager.RANDOM_SEED
for i in ITERATIONS:
rng.randf()
rng.seed = RANDOM_SEED
func benchmark_global_scope_randi_range() -> void:
seed(RANDOM_SEED)
seed(Manager.RANDOM_SEED)
for i in ITERATIONS:
randi_range(1234, 5678)
seed(RANDOM_SEED)
func benchmark_randi_range() -> void:
rng.seed = RANDOM_SEED
rng.seed = Manager.RANDOM_SEED
for i in ITERATIONS:
rng.randi_range(1234, 5678)
rng.seed = RANDOM_SEED
func benchmark_global_scope_randf_range() -> void:
seed(RANDOM_SEED)
seed(Manager.RANDOM_SEED)
for i in ITERATIONS:
randf_range(1234.0, 5678.0)
seed(RANDOM_SEED)
func benchmark_randf_range() -> void:
rng.seed = RANDOM_SEED
rng.seed = Manager.RANDOM_SEED
for i in ITERATIONS:
rng.randf_range(1234.0, 5678.0)
rng.seed = RANDOM_SEED
func benchmark_global_scope_randfn() -> void:
seed(RANDOM_SEED)
seed(Manager.RANDOM_SEED)
for i in ITERATIONS:
randfn(10.0, 2.0)
seed(RANDOM_SEED)
func benchmark_randfn() -> void:
rng.seed = RANDOM_SEED
rng.seed = Manager.RANDOM_SEED
for i in ITERATIONS:
rng.randfn(10.0, 2.0)
rng.seed = RANDOM_SEED
func benchmark_global_scope_randomize() -> void:
seed(RANDOM_SEED)
seed(Manager.RANDOM_SEED)
for i in ITERATIONS:
randomize()
seed(RANDOM_SEED)
func benchmark_randomize() -> void:
rng.seed = RANDOM_SEED
rng.seed = Manager.RANDOM_SEED
for i in ITERATIONS:
rng.randomize()
rng.seed = RANDOM_SEED

View File

@@ -0,0 +1,18 @@
// Similar to GDScript Array benchmarks, but using C# Array instead
public partial class Array : Benchmark
{
public void BenchmarkFillLoop()
{
int[] array = new int[10_000_000];
for(int i = 0; i < array.Length; i++)
{ array[i] = 1234; }
}
public void BenchmarkFillMethod()
{
int[] array = new int[10_000_000];
System.Array.Fill(array, 1234);
}
}

View File

@@ -0,0 +1,7 @@
public partial class Control : Benchmark
{
public void BenchmarkControl()
{
// An empty test, to act as a control
}
}

View File

@@ -0,0 +1,26 @@
// Similar to GDScript for_loop benchmarks, but using C#
public partial class ForLoop : Benchmark
{
private const int ITERATIONS = 1_000_000;
private int number = 0;
public void BenchmarkLoopAdd()
{
for(int i = 0; i < ITERATIONS; i++)
{
number += 1;
}
}
public void BenchmarkLoopCall()
{
for(int i = 0; i < ITERATIONS; i++)
{
Function();
}
}
public virtual void Function(){}
}

120
benchmarks/csharp/List.cs Normal file
View File

@@ -0,0 +1,120 @@
using System.Collections.Generic;
// Similar to GDScript Array benchmarks, but using C# List instead
public partial class List : Benchmark
{
private const int ITERATIONS = 2_000_000;
public void BenchmarkInt32List()
{
List<int> list = new List<int>();
for(int i = 0; i < ITERATIONS; i++)
{ list.Add(i); }
for(int i = 0; i < ITERATIONS; i++)
{ list[i] = 0; }
for(int i = 0; i < ITERATIONS; i++)
{ list.RemoveAt(list.Count - 1); }
}
public void BenchmarkInt64List()
{
List<long> list = new List<long>();
for(int i = 0; i < ITERATIONS; i++)
{ list.Add(i); }
for(int i = 0; i < ITERATIONS; i++)
{ list[i] = 0; }
for(int i = 0; i < ITERATIONS; i++)
{ list.RemoveAt(list.Count - 1); }
}
public void BenchmarkFloat32List()
{
List<float> list = new List<float>();
for(int i = 0; i < ITERATIONS; i++)
{ list.Add(i); }
for(int i = 0; i < ITERATIONS; i++)
{ list[i] = 0; }
for(int i = 0; i < ITERATIONS; i++)
{ list.RemoveAt(list.Count - 1); }
}
public void BenchmarkFloat64List()
{
List<double> list = new List<double>();
for(int i = 0; i < ITERATIONS; i++)
{ list.Add(i); }
for(int i = 0; i < ITERATIONS; i++)
{ list[i] = 0; }
for(int i = 0; i < ITERATIONS; i++)
{ list.RemoveAt(list.Count - 1); }
}
public void BenchmarkVector2List()
{
List<Godot.Vector2> list = new List<Godot.Vector2>();
for(int i = 0; i < ITERATIONS; i++)
{ list.Add(new Godot.Vector2(i, i)); }
for(int i = 0; i < ITERATIONS; i++)
{ list[i] = Godot.Vector2.Zero; }
for(int i = 0; i < ITERATIONS; i++)
{ list.RemoveAt(list.Count - 1); }
}
public void BenchmarkVector3List()
{
List<Godot.Vector3> list = new List<Godot.Vector3>();
for(int i = 0; i < ITERATIONS; i++)
{ list.Add(new Godot.Vector3(i, i, i)); }
for(int i = 0; i < ITERATIONS; i++)
{ list[i] = Godot.Vector3.Zero; }
for(int i = 0; i < ITERATIONS; i++)
{ list.RemoveAt(list.Count - 1); }
}
public void BenchmarkColorList()
{
List<Godot.Color> list = new List<Godot.Color>();
for(int i = 0; i < ITERATIONS; i++)
{ list.Add(new Godot.Color(i, i, i, 1.0f)); }
for(int i = 0; i < ITERATIONS; i++)
{ list[i] = Godot.Colors.Black; }
for(int i = 0; i < ITERATIONS; i++)
{ list.RemoveAt(list.Count - 1); }
}
public void BenchmarkStringList()
{
List<string> list = new List<string>();
for(int i = 0; i < ITERATIONS; i++)
{ list.Add("Godot " + i.ToString()); }
for(int i = 0; i < ITERATIONS; i++)
{ list[i] = ""; }
for(int i = 0; i < ITERATIONS; i++)
{ list.RemoveAt(list.Count - 1); }
}
}

View File

@@ -0,0 +1,128 @@
using System;
using Godot;
// Identical benchmark to "rigid_body_2d.gd" but written in C#
public partial class RigidBody2D : Benchmark
{
const bool VISUALIZE = true;
const double SPREAD_H = 1600.0f;
const double SPREAD_V = 800.0f;
private WorldBoundaryShape2D boundary_shape = new WorldBoundaryShape2D();
private RectangleShape2D SquareShape = new RectangleShape2D();
private CircleShape2D CircleShape = new CircleShape2D();
private QuadMesh SquareMesh = new QuadMesh();
private SphereMesh CircleMesh = new SphereMesh();
public RigidBody2D()
{
SquareMesh.Size = new Vector2(20.0f, 20.0f);
CircleMesh.Radius = 10.0f;
CircleMesh.Height = 20.0f;
test_physics = true;
test_idle = true;
}
public Node2D SetupScene(Func<bool, Godot.RigidBody2D.CcdMode, Godot.RigidBody2D> create_body_func, bool unique_shape, Godot.RigidBody2D.CcdMode ccd_mode, bool boundary, int num_shapes)
{
Node2D scene_root = new Node2D();
if(VISUALIZE)
{
Camera2D camera = new Camera2D() { Position = new Vector2(0.0f, -100.0f), Zoom = new Vector2(0.5f, 0.5f) };
scene_root.AddChild(camera);
}
if (boundary)
{
StaticBody2D pit = new StaticBody2D();
pit.AddChild(CreateWall(new Vector2((float)SPREAD_H, 0.0f), -0.1f));
pit.AddChild(CreateWall(new Vector2(-(float)SPREAD_H, 0.0f), 0.1f));
scene_root.AddChild(pit);
}
for(int i = 0; i < num_shapes; i++)
{
Godot.RigidBody2D body = create_body_func(unique_shape, ccd_mode);
body.Position = new Vector2((float)GD.RandRange(-SPREAD_H, SPREAD_H), (float)GD.RandRange(0.0d, -SPREAD_V));
scene_root.AddChild(body);
}
return scene_root;
}
public CollisionShape2D CreateWall(Vector2 position, float rotation)
{
return new CollisionShape2D() {Shape = boundary_shape, Position = position, Rotation = rotation };
}
public Godot.RigidBody2D CreateRandomShape(bool unique_shape, Godot.RigidBody2D.CcdMode ccd_mode)
{
switch(GD.RandRange(0,1))
{
case 0: return CreateSquare(unique_shape, ccd_mode);
default: return CreateCircle(unique_shape, ccd_mode);
}
}
public Godot.RigidBody2D CreateSquare(bool unique_shape, Godot.RigidBody2D.CcdMode ccd_mode)
{
Godot.RigidBody2D rigid_body = new Godot.RigidBody2D();
CollisionShape2D collision_shape = new CollisionShape2D();
rigid_body.ContinuousCd = ccd_mode;
if(VISUALIZE) { rigid_body.AddChild(new MeshInstance2D(){ Mesh = SquareMesh }); }
if (unique_shape) { collision_shape.Shape = new RectangleShape2D(); }
else { collision_shape.Shape = SquareShape; }
rigid_body.AddChild(collision_shape);
return rigid_body;
}
public Godot.RigidBody2D CreateCircle(bool unique_shape, Godot.RigidBody2D.CcdMode ccd_mode)
{
Godot.RigidBody2D rigid_body = new Godot.RigidBody2D();
CollisionShape2D collision_shape = new CollisionShape2D();
rigid_body.ContinuousCd = ccd_mode;
if(VISUALIZE) { rigid_body.AddChild(new MeshInstance2D(){ Mesh = CircleMesh }); }
if (unique_shape) { collision_shape.Shape = new CircleShape2D(); }
else { collision_shape.Shape = CircleShape; }
rigid_body.AddChild(collision_shape);
return rigid_body;
}
public Node2D Benchmark2000RigidBody2DSquares()
{
return SetupScene(CreateSquare, false, Godot.RigidBody2D.CcdMode.Disabled, true, 2000);
}
public Node2D Benchmark2000RigidBody2DCircles()
{
return SetupScene(CreateCircle, false, Godot.RigidBody2D.CcdMode.Disabled, true, 2000);
}
public Node2D Benchmark2000RigidBody2DMixed()
{
return SetupScene(CreateRandomShape, false, Godot.RigidBody2D.CcdMode.Disabled, true, 2000);
}
public Node2D Benchmark2000RigidBody2DUnique()
{
return SetupScene(CreateRandomShape, true, Godot.RigidBody2D.CcdMode.Disabled, true, 2000);
}
public Node2D Benchmark2000RigidBody2DContinuous()
{
return SetupScene(CreateRandomShape, false, Godot.RigidBody2D.CcdMode.CastShape, true, 2000);
}
public Node2D Benchmark2000RigidBody2DUnbound()
{
return SetupScene(CreateRandomShape, false, Godot.RigidBody2D.CcdMode.Disabled, false, 2000);
}
}

View File

@@ -0,0 +1,128 @@
using System;
using Godot;
// Identical benchmark to "rigid_body_3d.gd" but written in C#
public partial class RigidBody3D : Benchmark
{
const bool VISUALIZE = true;
const double SPREAD_H = 20.0f;
const double SPREAD_V = 10.0f;
private WorldBoundaryShape3D boundary_shape = new WorldBoundaryShape3D();
private BoxShape3D BoxShape = new BoxShape3D();
private SphereShape3D SphereShape = new SphereShape3D();
private BoxMesh BoxMesh = new BoxMesh();
private SphereMesh SphereMesh = new SphereMesh();
public RigidBody3D()
{
test_physics = true;
test_idle = true;
}
public Node3D SetupScene(Func<bool, bool, Godot.RigidBody3D> create_body_func, bool unique_shape, bool ccd_mode, bool boundary, int num_shapes)
{
Node3D scene_root = new Node3D();
if(VISUALIZE)
{
Camera3D camera = new Camera3D() { Position = new Vector3(0.0f, 20.0f, 20.0f) };
camera.RotateX(-0.8f);
scene_root.AddChild(camera);
}
if (boundary)
{
StaticBody3D pit = new StaticBody3D();
pit.AddChild(CreateWall(new Vector3((float)SPREAD_H, 0.0f, 0.0f), new Vector3(0.0f, 0.0f, 0.2f)));
pit.AddChild(CreateWall(new Vector3(0.0f, 0.0f, (float)SPREAD_H), new Vector3(-0.2f, 0.0f, 0.0f)));
pit.AddChild(CreateWall(new Vector3(-(float)SPREAD_H, 0.0f, 0.0f), new Vector3(0.0f, 0.0f, -0.2f)));
pit.AddChild(CreateWall(new Vector3(0.0f, 0.0f, -(float)SPREAD_H), new Vector3(0.2f, 0.0f, 0.0f)));
scene_root.AddChild(pit);
}
for(int i = 0; i < num_shapes; i++)
{
Godot.RigidBody3D body = create_body_func(unique_shape, ccd_mode);
body.Position = new Vector3((float)GD.RandRange(-SPREAD_H, SPREAD_H), (float)GD.RandRange(0.0d, SPREAD_V), (float)GD.RandRange(-SPREAD_H, SPREAD_H));
scene_root.AddChild(body);
}
return scene_root;
}
public CollisionShape3D CreateWall(Vector3 position, Vector3 rotation)
{
return new CollisionShape3D() {Shape = boundary_shape, Position = position, Rotation = rotation };
}
public Godot.RigidBody3D CreateRandomShape(bool unique_shape, bool ccd_mode)
{
switch(GD.RandRange(0,1))
{
case 0: return CreateBox(unique_shape, ccd_mode);
default: return CreateSphere(unique_shape, ccd_mode);
}
}
public Godot.RigidBody3D CreateBox(bool unique_shape, bool ccd_mode)
{
Godot.RigidBody3D rigid_body = new Godot.RigidBody3D();
CollisionShape3D collision_shape = new CollisionShape3D();
rigid_body.ContinuousCd = ccd_mode;
if(VISUALIZE) { rigid_body.AddChild(new MeshInstance3D(){ Mesh = BoxMesh }); }
if (unique_shape) { collision_shape.Shape = new BoxShape3D(); }
else { collision_shape.Shape = BoxShape; }
rigid_body.AddChild(collision_shape);
return rigid_body;
}
public Godot.RigidBody3D CreateSphere(bool unique_shape, bool ccd_mode)
{
Godot.RigidBody3D rigid_body = new Godot.RigidBody3D();
CollisionShape3D collision_shape = new CollisionShape3D();
rigid_body.ContinuousCd = ccd_mode;
if(VISUALIZE) { rigid_body.AddChild(new MeshInstance3D(){ Mesh = SphereMesh }); }
if (unique_shape) { collision_shape.Shape = new SphereShape3D(); }
else { collision_shape.Shape = SphereShape; }
rigid_body.AddChild(collision_shape);
return rigid_body;
}
public Node3D Benchmark2000RigidBody3DSquares()
{
return SetupScene(CreateBox, false, false, true, 2000);
}
public Node3D Benchmark2000RigidBody3DCircles()
{
return SetupScene(CreateSphere, false, false, true, 2000);
}
public Node3D Benchmark2000RigidBody3DMixed()
{
return SetupScene(CreateRandomShape, false, false, true, 2000);
}
public Node3D Benchmark2000RigidBody3DUnique()
{
return SetupScene(CreateRandomShape, true, false, true, 2000);
}
public Node3D Benchmark2000RigidBody3DContinuous()
{
return SetupScene(CreateRandomShape, false, true, true, 2000);
}
public Node3D Benchmark2000RigidBody3DUnbound()
{
return SetupScene(CreateRandomShape, false, false, false, 2000);
}
}

View File

@@ -0,0 +1,6 @@
extends Benchmark
# An empty test, to act as a control
func benchmark_control():
pass

View File

@@ -0,0 +1,119 @@
extends Benchmark
const VISUALIZE := true
const SPREAD_H := 1600.0;
const SPREAD_V := 800.0;
var boundary_shape := WorldBoundaryShape2D.new()
var square_shape := RectangleShape2D.new()
var circle_shape := CircleShape2D.new()
var square_mesh := QuadMesh.new()
var circle_mesh := SphereMesh.new()
func _init() -> void:
square_mesh.size = Vector2(20.0, 20.0);
circle_mesh.radius = 10.0;
circle_mesh.height = 20.0;
test_physics = true
test_idle = true
func setup_scene(create_body_func: Callable, unique_shape: bool, ccd_mode: RigidBody2D.CCDMode, boundary: bool, num_shapes: int) -> Node2D:
var scene_root := Node2D.new()
if VISUALIZE:
var camera := Camera2D.new()
camera.position = Vector2(0.0, -100.0)
camera.zoom = Vector2(0.5, 0.5)
scene_root.add_child(camera)
if boundary:
var pit := StaticBody2D.new();
pit.add_child(create_wall(Vector2(SPREAD_H, 0.0), -0.1))
pit.add_child(create_wall(Vector2(-SPREAD_H, 0.0), 0.1))
scene_root.add_child(pit);
for _i in num_shapes:
var body: RigidBody2D = create_body_func.call(unique_shape, ccd_mode)
body.position = Vector2(randf_range(-SPREAD_H, SPREAD_H), randf_range(0.0, -SPREAD_V))
scene_root.add_child(body)
return scene_root
func create_wall(position: Vector2, rotation: float) -> CollisionShape2D:
var wall := CollisionShape2D.new()
wall.shape = boundary_shape
wall.position = position
wall.rotation = rotation
return wall
func create_random_shape(unique_shape: bool, ccd_mode: RigidBody2D.CCDMode) -> RigidBody2D:
match randi_range(0, 1):
0: return create_square(unique_shape, ccd_mode)
_: return create_circle(unique_shape, ccd_mode)
func create_square(unique_shape: bool, ccd_mode: RigidBody2D.CCDMode) -> RigidBody2D:
var rigid_body := RigidBody2D.new()
var collision_shape := CollisionShape2D.new()
rigid_body.continuous_cd = ccd_mode
if VISUALIZE:
var mesh_instance := MeshInstance2D.new()
mesh_instance.mesh = square_mesh
rigid_body.add_child(mesh_instance)
if unique_shape:
collision_shape.shape = RectangleShape2D.new()
else:
# Reuse existing shape.
collision_shape.shape = square_shape
rigid_body.add_child(collision_shape)
return rigid_body
func create_circle(unique_shape: bool, ccd_mode: RigidBody2D.CCDMode) -> RigidBody2D:
var rigid_body := RigidBody2D.new()
var collision_shape := CollisionShape2D.new()
rigid_body.continuous_cd = ccd_mode
if VISUALIZE:
var mesh_instance := MeshInstance2D.new()
mesh_instance.mesh = circle_mesh
rigid_body.add_child(mesh_instance)
if unique_shape:
collision_shape.shape = CircleShape2D.new()
else:
# Reuse existing shape.
collision_shape.shape = circle_shape
rigid_body.add_child(collision_shape)
return rigid_body
func benchmark_2000_rigid_body_2d_squares() -> Node2D:
return setup_scene(create_square, false, RigidBody2D.CCD_MODE_DISABLED, true, 2000)
func benchmark_2000_rigid_body_2d_circles() -> Node2D:
return setup_scene(create_circle, false, RigidBody2D.CCD_MODE_DISABLED, true, 2000)
func benchmark_2000_rigid_body_2d_mixed() -> Node2D:
return setup_scene(create_random_shape, false, RigidBody2D.CCD_MODE_DISABLED, true, 2000)
func benchmark_2000_rigid_body_2d_unique() -> Node2D:
return setup_scene(create_random_shape, true, RigidBody2D.CCD_MODE_DISABLED, true, 2000)
func benchmark_2000_rigid_body_2d_continuous() -> Node2D:
return setup_scene(create_random_shape, false, RigidBody2D.CCD_MODE_CAST_SHAPE, true, 2000)
func benchmark_2000_rigid_body_2d_unbound() -> Node2D:
return setup_scene(create_random_shape, false, RigidBody2D.CCD_MODE_DISABLED, false, 2000)

View File

@@ -1,34 +1,69 @@
extends Benchmark
const VISUALIZE := true
const SPREAD_H := 20.0;
const SPREAD_V := 10.0;
var boundary_shape := WorldBoundaryShape3D.new()
var box_shape := BoxShape3D.new()
var sphere_shape := SphereShape3D.new()
var box_mesh := BoxMesh.new()
var sphere_mesh := SphereMesh.new()
func _init() -> void:
test_physics = true
test_idle = true
func setup_scene(create_body_func: Callable, unique_shape: bool, num_shapes: int) -> Node3D:
func setup_scene(create_body_func: Callable, unique_shape: bool, ccd_mode: bool, boundary: bool, num_shapes: int) -> Node3D:
var scene_root := Node3D.new()
var camera := Camera3D.new()
camera.position.y = 0.3
camera.position.z = 1.0
camera.rotate_x(-0.8)
scene_root.add_child(camera)
if VISUALIZE:
var camera := Camera3D.new()
camera.position = Vector3(0.0, 20.0, 20.0)
camera.rotate_x(-0.8)
scene_root.add_child(camera)
if boundary:
var pit := StaticBody3D.new();
pit.add_child(create_wall(Vector3(SPREAD_H, 0.0, 0.0), Vector3(0.0, 0.0, 0.2)))
pit.add_child(create_wall(Vector3(0.0, 0.0, SPREAD_H), Vector3(-0.2, 0.0, 0.0)))
pit.add_child(create_wall(Vector3(-SPREAD_H, 0.0, 0.0), Vector3(0.0, 0.0, -0.2)))
pit.add_child(create_wall(Vector3(0.0, 0.0, -SPREAD_H), Vector3(0.2, 0.0, 0.0)))
scene_root.add_child(pit);
for _i in num_shapes:
var box: RigidBody3D = create_body_func.call(unique_shape)
box.position.x = randf_range(-50, 50)
box.position.z = randf_range(-50, 50)
scene_root.add_child(box)
var body: RigidBody3D = create_body_func.call(unique_shape, ccd_mode)
body.position = Vector3(randf_range(-SPREAD_H, SPREAD_H), randf_range(0.0, SPREAD_V), randf_range(-SPREAD_H, SPREAD_H))
scene_root.add_child(body)
return scene_root
func create_box(unique_shape: bool) -> RigidBody3D:
var rigid_body := RigidBody3D.new()
func create_wall(position: Vector3, rotation: Vector3) -> CollisionShape3D:
var wall := CollisionShape3D.new()
wall.shape = boundary_shape
wall.position = position
wall.rotation = rotation
return wall
func create_random_shape(unique_shape: bool, ccd_mode: bool) -> RigidBody3D:
match randi_range(0, 1):
0: return create_box(unique_shape, ccd_mode)
_: return create_sphere(unique_shape, ccd_mode)
func create_box(unique_shape: bool, ccd_mode: bool) -> RigidBody3D:
var rigid_body := RigidBody3D.new()
var collision_shape := CollisionShape3D.new()
rigid_body.continuous_cd = ccd_mode
if VISUALIZE:
var mesh_instance := MeshInstance3D.new()
mesh_instance.mesh = box_mesh
rigid_body.add_child(mesh_instance)
if unique_shape:
collision_shape.shape = BoxShape3D.new()
else:
@@ -39,10 +74,16 @@ func create_box(unique_shape: bool) -> RigidBody3D:
return rigid_body
func create_sphere(unique_shape: bool) -> RigidBody3D:
func create_sphere(unique_shape: bool, ccd_mode: bool) -> RigidBody3D:
var rigid_body := RigidBody3D.new()
var collision_shape := CollisionShape3D.new()
rigid_body.continuous_cd = ccd_mode
if VISUALIZE:
var mesh_instance := MeshInstance3D.new()
mesh_instance.mesh = sphere_mesh
rigid_body.add_child(mesh_instance)
if unique_shape:
collision_shape.shape = SphereShape3D.new()
else:
@@ -53,17 +94,25 @@ func create_sphere(unique_shape: bool) -> RigidBody3D:
return rigid_body
func benchmark_7500_rigid_body_3d_shared_box_shape() -> Node3D:
return setup_scene(create_box, false, 7500)
func benchmark_2000_rigid_body_3d_boxes() -> Node3D:
return setup_scene(create_box, false, false, true, 2000)
func benchmark_7500_rigid_body_3d_unique_box_shape() -> Node3D:
return setup_scene(create_box, true, 7500)
func benchmark_2000_rigid_body_3d_spheres() -> Node3D:
return setup_scene(create_sphere, false, false, true, 2000)
func benchmark_7500_rigid_body_3d_shared_sphere_shape() -> Node3D:
return setup_scene(create_sphere, false, 7500)
func benchmark_2000_rigid_body_3d_mixed() -> Node3D:
return setup_scene(create_random_shape, false, false, true, 2000)
func benchmark_7500_rigid_body_3d_unique_sphere_shape() -> Node3D:
return setup_scene(create_sphere, true, 7500)
func benchmark_2000_rigid_body_3d_unique() -> Node3D:
return setup_scene(create_random_shape, true, false, true, 2000)
func benchmark_2000_rigid_body_3d_continuous() -> Node3D:
return setup_scene(create_random_shape, false, true, true, 2000)
func benchmark_2000_rigid_body_3d_unbound() -> Node3D:
return setup_scene(create_random_shape, false, false, false, 2000)

View File

@@ -7,7 +7,7 @@ func _init():
class TestScene extends Node3D:
var scene = preload("res://benchmarks/rendering/very_large_scene.tscn")
var scene = preload("res://supplemental/very_large_scene.tscn")
var instance
var world_env: WorldEnvironment = null

View File

@@ -7,7 +7,7 @@ func _init():
class TestScene extends Node3D:
var sponza_scene = preload("res://benchmarks/rendering/sponza.tscn")
var sponza_scene = preload("res://supplemental/sponza.tscn")
var sponza
var world_env: WorldEnvironment = null

15
main.gd
View File

@@ -1,7 +1,5 @@
extends Panel
const RANDOM_SEED = 0x60d07
var items := []
# Prefix variables with `arg_` to have them automatically be parsed from command line arguments
@@ -11,11 +9,9 @@ var arg_save_json := ""
var arg_run_benchmarks := false
@onready var tree := $Tree as Tree
var categories := {}
func _ready() -> void:
# Use a fixed random seed to improve reproducibility of results.
seed(RANDOM_SEED)
# Parse valid command-line arguments of the form `--key=value` into member variables.
for argument in OS.get_cmdline_user_args():
if not argument.begins_with("--"):
@@ -50,7 +46,6 @@ func _ready() -> void:
tree.set_column_title(5, "Main Thread Time")
var root := tree.create_item()
var categories := {}
for test_id in Manager.get_test_ids():
var test_name := test_id.pretty_name()
@@ -75,10 +70,10 @@ func _ready() -> void:
# from the interface after the fact.
item.set_checked(0, true)
if arg_include_benchmarks:
if not path.match(arg_include_benchmarks):
if not path.matchn(arg_include_benchmarks):
item.set_checked(0, false)
if arg_exclude_benchmarks:
if path.match(arg_exclude_benchmarks):
if path.matchn(arg_exclude_benchmarks):
item.set_checked(0, false)
var results := Manager.get_test_result_as_dict(test_id)
@@ -100,12 +95,16 @@ func _ready() -> void:
func _on_SelectAll_pressed() -> void:
for category in categories.values():
category.collapsed = false
for item in items:
item.set_checked(0, true)
_on_Tree_item_edited()
func _on_SelectNone_pressed() -> void:
for category in categories.values():
category.collapsed = true
for item in items:
item.set_checked(0, false)
_on_Tree_item_edited()

View File

@@ -1,5 +1,7 @@
extends Node
const RANDOM_SEED = 0x60d07
class Results:
var render_cpu := 0.0
var render_gpu := 0.0
@@ -10,6 +12,7 @@ class Results:
class TestID:
var name : String
var category : String
var language : String
func pretty_name() -> String:
return name.capitalize()
@@ -24,24 +27,29 @@ class TestID:
func test_ids_from_path(path: String) -> Array[TestID]:
var rv : Array[TestID] = []
if not path.ends_with(".gd"):
return rv
var script = load(path).new()
if not (script is Benchmark):
return rv
var bench_script : Benchmark = script
for method in bench_script.get_method_list():
if not method.name.begins_with("benchmark_"):
# Check for runnable tests.
for extension in languages.keys():
if not path.ends_with(extension):
continue
var test_id := TestID.new()
test_id.name = method.name.trim_prefix("benchmark_")
test_id.category = path.trim_prefix("res://benchmarks/").trim_suffix(".gd")
rv.push_back(test_id)
return rv
var bench_script = load(path).new()
for method in bench_script.get_method_list():
if not method.name.begins_with(languages[extension]["test_prefix"]):
continue
# This method is a runnable test. Push it onto the result
var test_id := TestID.new()
test_id.name = method.name.trim_prefix(languages[extension]["test_prefix"])
test_id.category = path.trim_prefix("res://benchmarks/").trim_suffix(extension)
test_id.language = extension
rv.push_back(test_id)
return rv
# List of supported languages and their styles.
var languages := {".gd": {"test_prefix": "benchmark_"}}
# List of benchmarks populated in `_ready()`.
var test_results := {}
@@ -72,6 +80,10 @@ func _ready():
RenderingServer.viewport_set_measure_render_time(get_tree().root.get_viewport_rid(),true)
set_process(false)
# Register script language compatibility
if Engine.has_singleton("GodotSharp"):
languages[".cs"] = {"test_prefix": "Benchmark"}
# Register contents of `benchmarks/` folder automatically.
for benchmark_path in dir_contents("res://benchmarks/"):
for test_id in test_ids_from_path(benchmark_path):
@@ -93,7 +105,9 @@ func benchmark(test_ids: Array[TestID], return_path: String) -> void:
for i in range(test_ids.size()):
DisplayServer.window_set_title("%d/%d - Running %s" % [i + 1, test_ids.size(), test_ids[i].pretty()])
print("Running benchmark %d of %d: %s" % [i + 1, test_ids.size(), test_ids[i]])
seed(RANDOM_SEED)
await run_test(test_ids[i])
print("Result: %s\n" % get_result_as_string(test_ids[i]))
DisplayServer.window_set_title("[DONE] %d benchmarks - Godot Benchmarks" % test_ids.size())
print_rich("[color=green][b]Done running %d benchmarks.[/b] Results JSON:[/color]\n" % test_ids.size())
@@ -111,6 +125,7 @@ func benchmark(test_ids: Array[TestID], return_path: String) -> void:
if return_path:
get_tree().change_scene_to_file(return_path)
else:
get_tree().queue_delete(get_tree())
get_tree().quit()
@@ -128,13 +143,15 @@ func run_test(test_id: TestID) -> void:
# Add a dummy child so that the above check works for subsequent reloads
get_tree().current_scene.add_child(Node.new())
var benchmark_script : Benchmark = load("res://benchmarks/%s.gd" % test_id.category).new()
var bench_script = load("res://benchmarks/%s%s" % [test_id.category, test_id.language]).new()
var results := Results.new()
# Call and time the function to be tested
var begin_time := Time.get_ticks_usec()
var bench_node = benchmark_script.call("benchmark_" + test_id.name)
var bench_node = bench_script.call(languages[test_id.language]["test_prefix"] + test_id.name)
results.time = (Time.get_ticks_usec() - begin_time) * 0.001
# Continue benchmarking if the function call has returned a node
var frames_captured := 0
if bench_node:
get_tree().current_scene.add_child(bench_node)
@@ -166,11 +183,21 @@ func run_test(test_id: TestID) -> void:
# metrics calculated on a per-second basis.
for metric in results.get_property_list():
if benchmark_script.get("test_" + metric.name) == false: # account for null
if bench_script.get("test_" + metric.name) == false: # account for null
results.set(metric.name, 0.0)
test_results[test_id] = results
func get_result_as_string(test_id: TestID) -> String:
# Returns all non-zero metrics formatted as a string
var rd := get_test_result_as_dict(test_id)
for key in rd.keys():
if rd[key] == 0.0:
rd.erase(key)
return JSON.stringify(rd)
func get_test_result_as_dict(test_id: TestID) -> Dictionary:
var result : Results = test_results[test_id]
var rv := {}

View File

@@ -29,6 +29,10 @@ window/size/viewport_width=1920
window/size/viewport_height=1080
window/stretch/mode="viewport"
[dotnet]
project/assembly_name="Godot Benchmarks"
[gui]
theme/default_theme_scale=1.5

View File

@@ -1,6 +1,7 @@
[gd_scene load_steps=6 format=3 uid="uid://33dqw5jcha0h"]
[ext_resource type="Script" path="res://benchmarks/rendering/camera_move.gd" id="1_vqkte"]
[ext_resource type="Script" path="res://supplemental/camera_move.gd" id="1_vqkte"]
[sub_resource type="Environment" id="Environment_itcjv"]