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** > **Note**
> >
> This project only supports Godot's `master` branch (4.0's development branch), > 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 ## 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 In the `_init()` function of your script, configure your benchmark by
initializing any desired `Benchmark` variables to true: 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. 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. 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. 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. 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 ### Implement the benchmark
@@ -57,6 +59,9 @@ Remember to follow the
when writing new scripts. Adding type hints is recommended whenever possible, when writing new scripts. Adding type hints is recommended whenever possible,
unless you are specifically benchmarking non-typed scripts. 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 ### Test the benchmark
1. Run the project in the editor. 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 extends Benchmark
const ITERATIONS = 10_000_000 const ITERATIONS = 10_000_000
const RANDOM_SEED = preload("res://main.gd").RANDOM_SEED
var rng := RandomNumberGenerator.new() var rng := RandomNumberGenerator.new()
func benchmark_global_scope_randi() -> void: func benchmark_global_scope_randi() -> void:
# Reset the random seed to improve reproducibility of this benchmark. seed(Manager.RANDOM_SEED)
seed(RANDOM_SEED)
for i in ITERATIONS: for i in ITERATIONS:
randi() randi()
# Reset the random seed again to improve reproducibility of other benchmarks.
seed(RANDOM_SEED)
func benchmark_randi() -> void: func benchmark_randi() -> void:
rng.seed = RANDOM_SEED rng.seed = Manager.RANDOM_SEED
for i in ITERATIONS: for i in ITERATIONS:
rng.randi() rng.randi()
rng.seed = RANDOM_SEED
func benchmark_global_scope_randf() -> void: func benchmark_global_scope_randf() -> void:
seed(RANDOM_SEED) seed(Manager.RANDOM_SEED)
for i in ITERATIONS: for i in ITERATIONS:
randf() randf()
seed(RANDOM_SEED)
func benchmark_randf() -> void: func benchmark_randf() -> void:
rng.seed = RANDOM_SEED rng.seed = Manager.RANDOM_SEED
for i in ITERATIONS: for i in ITERATIONS:
rng.randf() rng.randf()
rng.seed = RANDOM_SEED
func benchmark_global_scope_randi_range() -> void: func benchmark_global_scope_randi_range() -> void:
seed(RANDOM_SEED) seed(Manager.RANDOM_SEED)
for i in ITERATIONS: for i in ITERATIONS:
randi_range(1234, 5678) randi_range(1234, 5678)
seed(RANDOM_SEED)
func benchmark_randi_range() -> void: func benchmark_randi_range() -> void:
rng.seed = RANDOM_SEED rng.seed = Manager.RANDOM_SEED
for i in ITERATIONS: for i in ITERATIONS:
rng.randi_range(1234, 5678) rng.randi_range(1234, 5678)
rng.seed = RANDOM_SEED
func benchmark_global_scope_randf_range() -> void: func benchmark_global_scope_randf_range() -> void:
seed(RANDOM_SEED) seed(Manager.RANDOM_SEED)
for i in ITERATIONS: for i in ITERATIONS:
randf_range(1234.0, 5678.0) randf_range(1234.0, 5678.0)
seed(RANDOM_SEED)
func benchmark_randf_range() -> void: func benchmark_randf_range() -> void:
rng.seed = RANDOM_SEED rng.seed = Manager.RANDOM_SEED
for i in ITERATIONS: for i in ITERATIONS:
rng.randf_range(1234.0, 5678.0) rng.randf_range(1234.0, 5678.0)
rng.seed = RANDOM_SEED
func benchmark_global_scope_randfn() -> void: func benchmark_global_scope_randfn() -> void:
seed(RANDOM_SEED) seed(Manager.RANDOM_SEED)
for i in ITERATIONS: for i in ITERATIONS:
randfn(10.0, 2.0) randfn(10.0, 2.0)
seed(RANDOM_SEED)
func benchmark_randfn() -> void: func benchmark_randfn() -> void:
rng.seed = RANDOM_SEED rng.seed = Manager.RANDOM_SEED
for i in ITERATIONS: for i in ITERATIONS:
rng.randfn(10.0, 2.0) rng.randfn(10.0, 2.0)
rng.seed = RANDOM_SEED
func benchmark_global_scope_randomize() -> void: func benchmark_global_scope_randomize() -> void:
seed(RANDOM_SEED) seed(Manager.RANDOM_SEED)
for i in ITERATIONS: for i in ITERATIONS:
randomize() randomize()
seed(RANDOM_SEED)
func benchmark_randomize() -> void: func benchmark_randomize() -> void:
rng.seed = RANDOM_SEED rng.seed = Manager.RANDOM_SEED
for i in ITERATIONS: for i in ITERATIONS:
rng.randomize() 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 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 box_shape := BoxShape3D.new()
var sphere_shape := SphereShape3D.new() var sphere_shape := SphereShape3D.new()
var box_mesh := BoxMesh.new()
var sphere_mesh := SphereMesh.new()
func _init() -> void: func _init() -> void:
test_physics = true test_physics = true
test_idle = 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 scene_root := Node3D.new()
var camera := Camera3D.new()
camera.position.y = 0.3 if VISUALIZE:
camera.position.z = 1.0 var camera := Camera3D.new()
camera.rotate_x(-0.8) camera.position = Vector3(0.0, 20.0, 20.0)
scene_root.add_child(camera) 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: for _i in num_shapes:
var box: RigidBody3D = create_body_func.call(unique_shape) var body: RigidBody3D = create_body_func.call(unique_shape, ccd_mode)
box.position.x = randf_range(-50, 50) body.position = Vector3(randf_range(-SPREAD_H, SPREAD_H), randf_range(0.0, SPREAD_V), randf_range(-SPREAD_H, SPREAD_H))
box.position.z = randf_range(-50, 50) scene_root.add_child(body)
scene_root.add_child(box)
return scene_root return scene_root
func create_box(unique_shape: bool) -> RigidBody3D: func create_wall(position: Vector3, rotation: Vector3) -> CollisionShape3D:
var rigid_body := RigidBody3D.new() 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() 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: if unique_shape:
collision_shape.shape = BoxShape3D.new() collision_shape.shape = BoxShape3D.new()
else: else:
@@ -39,10 +74,16 @@ func create_box(unique_shape: bool) -> RigidBody3D:
return rigid_body 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 rigid_body := RigidBody3D.new()
var collision_shape := CollisionShape3D.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: if unique_shape:
collision_shape.shape = SphereShape3D.new() collision_shape.shape = SphereShape3D.new()
else: else:
@@ -53,17 +94,25 @@ func create_sphere(unique_shape: bool) -> RigidBody3D:
return rigid_body return rigid_body
func benchmark_7500_rigid_body_3d_shared_box_shape() -> Node3D: func benchmark_2000_rigid_body_3d_boxes() -> Node3D:
return setup_scene(create_box, false, 7500) return setup_scene(create_box, false, false, true, 2000)
func benchmark_7500_rigid_body_3d_unique_box_shape() -> Node3D: func benchmark_2000_rigid_body_3d_spheres() -> Node3D:
return setup_scene(create_box, true, 7500) return setup_scene(create_sphere, false, false, true, 2000)
func benchmark_7500_rigid_body_3d_shared_sphere_shape() -> Node3D: func benchmark_2000_rigid_body_3d_mixed() -> Node3D:
return setup_scene(create_sphere, false, 7500) return setup_scene(create_random_shape, false, false, true, 2000)
func benchmark_7500_rigid_body_3d_unique_sphere_shape() -> Node3D: func benchmark_2000_rigid_body_3d_unique() -> Node3D:
return setup_scene(create_sphere, true, 7500) 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: 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 instance
var world_env: WorldEnvironment = null var world_env: WorldEnvironment = null

View File

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

15
main.gd
View File

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

View File

@@ -1,5 +1,7 @@
extends Node extends Node
const RANDOM_SEED = 0x60d07
class Results: class Results:
var render_cpu := 0.0 var render_cpu := 0.0
var render_gpu := 0.0 var render_gpu := 0.0
@@ -10,6 +12,7 @@ class Results:
class TestID: class TestID:
var name : String var name : String
var category : String var category : String
var language : String
func pretty_name() -> String: func pretty_name() -> String:
return name.capitalize() return name.capitalize()
@@ -24,24 +27,29 @@ class TestID:
func test_ids_from_path(path: String) -> Array[TestID]: func test_ids_from_path(path: String) -> Array[TestID]:
var rv : Array[TestID] = [] var rv : Array[TestID] = []
if not path.ends_with(".gd"):
return rv
var script = load(path).new() # Check for runnable tests.
if not (script is Benchmark): for extension in languages.keys():
return rv if not path.ends_with(extension):
var bench_script : Benchmark = script
for method in bench_script.get_method_list():
if not method.name.begins_with("benchmark_"):
continue continue
var test_id := TestID.new() var bench_script = load(path).new()
test_id.name = method.name.trim_prefix("benchmark_") for method in bench_script.get_method_list():
test_id.category = path.trim_prefix("res://benchmarks/").trim_suffix(".gd") if not method.name.begins_with(languages[extension]["test_prefix"]):
rv.push_back(test_id) continue
return rv
# 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()`. # List of benchmarks populated in `_ready()`.
var test_results := {} var test_results := {}
@@ -72,6 +80,10 @@ func _ready():
RenderingServer.viewport_set_measure_render_time(get_tree().root.get_viewport_rid(),true) RenderingServer.viewport_set_measure_render_time(get_tree().root.get_viewport_rid(),true)
set_process(false) set_process(false)
# Register script language compatibility
if Engine.has_singleton("GodotSharp"):
languages[".cs"] = {"test_prefix": "Benchmark"}
# Register contents of `benchmarks/` folder automatically. # Register contents of `benchmarks/` folder automatically.
for benchmark_path in dir_contents("res://benchmarks/"): for benchmark_path in dir_contents("res://benchmarks/"):
for test_id in test_ids_from_path(benchmark_path): 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()): for i in range(test_ids.size()):
DisplayServer.window_set_title("%d/%d - Running %s" % [i + 1, test_ids.size(), test_ids[i].pretty()]) 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]]) print("Running benchmark %d of %d: %s" % [i + 1, test_ids.size(), test_ids[i]])
seed(RANDOM_SEED)
await run_test(test_ids[i]) 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()) 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()) 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: if return_path:
get_tree().change_scene_to_file(return_path) get_tree().change_scene_to_file(return_path)
else: else:
get_tree().queue_delete(get_tree())
get_tree().quit() 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 # Add a dummy child so that the above check works for subsequent reloads
get_tree().current_scene.add_child(Node.new()) 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() var results := Results.new()
# Call and time the function to be tested
var begin_time := Time.get_ticks_usec() 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 results.time = (Time.get_ticks_usec() - begin_time) * 0.001
# Continue benchmarking if the function call has returned a node
var frames_captured := 0 var frames_captured := 0
if bench_node: if bench_node:
get_tree().current_scene.add_child(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. # metrics calculated on a per-second basis.
for metric in results.get_property_list(): 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) results.set(metric.name, 0.0)
test_results[test_id] = results 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: func get_test_result_as_dict(test_id: TestID) -> Dictionary:
var result : Results = test_results[test_id] var result : Results = test_results[test_id]
var rv := {} var rv := {}

View File

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

View File

@@ -1,6 +1,7 @@
[gd_scene load_steps=6 format=3 uid="uid://33dqw5jcha0h"] [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"] [sub_resource type="Environment" id="Environment_itcjv"]