Skip to content

Instantly share code, notes, and snippets.

@simpuid
Last active June 4, 2020 22:41
Show Gist options
  • Save simpuid/35bd3fa74d052ec1e0bae5453f9fdd12 to your computer and use it in GitHub Desktop.
Save simpuid/35bd3fa74d052ec1e0bae5453f9fdd12 to your computer and use it in GitHub Desktop.
GSoC proposal for Godot Engine

Custom Performance Monitors and Custom Profilers in Godot Engine

Personal Information

College Information

  • College: Indian Institute of Technology, Roorkee, India

  • Major: Computer Science and Engineering

  • Current Year: 2nd Year

  • Degree: Bachelor of Technology

Personal Introduction

Hello there!

​ I am Utkarsh, a sophomore with major in computer science at the Indian Institute of Technology Roorkee, India. I have been interested in game development since my middle school days way before Godot was open-sourced. I was introduced to game development through Game Maker. Eventually, I gained some programming experience and tried other high-level engines and low-level frameworks. I developed many prototypes (mostly 2d due to a lack of 3d modelling skills). I have some experience in digital arts, thanks to all those sprites I drew for my earlier games. So, I have a keen interest in games, game engines, programming etc.

Programming Experience

  • In-depth knowledge of C++, interested in the modern part of it.
  • Intermediate knowledge of Rust and it's Ownership model. It helps while programming in other languages too.
  • Fluent in C#, Java, Kotlin, Go and a little bit in Haxe, Python, and Javascript
  • Hands-On experience in SDL2 (Go and C/C++), SFML (C++), Unity (C#), LibGDX (Java and Kotlin), MonoGame(C#) and other similar frameworks/engines.
  • Aware of multiple design patterns used in programming and learning more about it.
  • Some knowledge of OpenGL, Meson, CMake

Past Projects

Some of my past projects are either prototypes or in collaboration with other students/friends. Here are some other personal projects complete enough for worth mentioning:

Battle-Factory

Itch.io Link

GitHub Source

  • It is a turn-based, multiplayer (different systems) strategy game.
  • Works between 2 players within the same sub-net (Unless ports are forwarded).
  • It follows p2p architecture, each player has authority over its own units.
  • The message system was developed by hand for limited data types, I was unaware of C#'s serialization libraries, but my implementation works anyway.
  • That message system is used to communicate between players about their actions turn by turn.
  • The networking part is developed over an abstracted TCP socket library called Telepathy.
  • Works on mobile and desktop even cross-play is possible.
  • The whole game is developed in Unity.
  • Developed in a limited 1-month time frame.

Chip8

GitHub Source

  • An emulator for intermediate programming language CHIP-8
  • Implemented in Go.
  • Uses SDL2 for window management and renders in software mode. The renderer of the emulator is developed over the SDL2's surface blit feature.
  • Supports configuration for key mapping, window parameter, emulation speed, etc.
  • Most of the standard ROMs works without error.

Stacky

GitHub Source

  • An interpreted, esoteric, programming language involving stacks and only stacks. Useful in code golfing.

  • The basic element of this language is stack. You can move elements between stacks or clone elements from one stack to another, that's all.

  • Special stacks are provided to perform specialised tasks.

  • The language is Turing complete, thanks to Brainfuck2Stacky converter in the repository.

  • Implemented in C++ using only the Standard Library (No external dependency)

  • There are many perks to using stack as a basic element of language. Check readme to know more about those perks.

  • This is more like a proof-of-concept project and it works perfectly, another implementation is planned with programmable special stacks, variants as element and some debugging features thanks to Godot's usage of Variant for inspiration.

Experience with Godot

My experience with Godot is rather new. I like the node system very much. It is similar to Froggy's Component Graph System. I developed some prototypes with the help of docs. Custom language for a game engine seems like a bad idea at first, but I can see the benefit in Hot Reloading, Expression etc. I also like the idea of using the engine for its editor. It helps while developing UI for plugins. The use of Scene Editor to develop UI for a custom plugin is an amazing idea. I haven't touched the 3D part yet. I hope to gain enough experience and speed before October's Ludum Dare.

Contribution to Godot

Bug Fixes

  • Changed set_meta to remove_meta in the CLEAR_GUIDES menu option #34114.
  • Remove update condition from LineEdit::update_placeholder_width #37123.
  • Fixes transform gizmo position when Node has default transform #37161.
  • Fixes unnecessary change in animation length property while using the slider #37083.
  • Sync mouse_filter property in SpinBox with internal LineEdit #36660.
  • Fixed Node renaming bug #35319.
  • Curve2D and Curve3D revert bug #36644.

Enhancements

The Problem

Accept it or not, debugging games is very hard. Games involve multiple factors like player input, randomness, etc. Each session of debugging is different due to these factors. Breakpoints are good for finding bugs, but they freeze the whole game. Bugs/Reasons for bad performance can't be found easily using breakpoints. Visual tools are used to find them. One of the visual tools is Godot's Performance Monitor.

Custom Performance Monitors

Godot's implementation of Performance Monitor monitors the core features like frame rate, physics time, draw calls, memory usage, etc. It visualizes them with the help of line graphs. Users have to roll their monitoring systems to monitor custom features. Godot's Performance Monitor can be used to monitor user-defined features. This proposal provides a solution to add custom performance monitor support in Godot. Adding a custom monitoring system will

  • Save the implementation time of a separate user-made monitoring system for each project.
  • Most of the user-made monitoring system renders graphs/values on top of the game. So a custom monitoring system in the editor will save screen space. Testing becomes easier.
  • Profiling games on remote devices like android phones, iPhone, iPad, etc becomes easier.

Custom monitors are not limited to only profiling. They can be used to monitor other values like bullet count, score, accelerometer's velocity, etc.

Custom Profilers

Custom Performance Monitors can profile the int/float value during a debug session. They cover most of the use cases encountered in small/medium games. Large games need more flexibility. They need to profile different types of data. Custom Profilers can solve this problem. It can help to collect, process and visualize non-generic profiling data like:

  • State of different sensors in remote device.
  • Taps received by input system.
  • Turn history in a turn based game.
  • Procedural generators' state.

This proposal provides a solution to use existing debugger code to implement Custom Profiler API.

Solution Overview

Editor refers to engine instance running Godot Editor

Game refers to engine instance running game project

Custom Performance Monitors

Game Side

  • Game adds custom monitors using functions defined in Performance class.
  • func add_monitor(id: String, callable: Callable, args: Array) adds a monitor with supplied String as id, Callable callable is called with Array args as arguments to get monitor values in future ticks.
  • func remove_monitor(id: String) removes the monitor associated with id.
  • Callable is called in the Game and it should return either an int or a float
  • Game serializes all returned values of added Callables to Array<Varaint> and sends them as packet performance:monitor_frame to Editor.
  • Game serializes the custom monitor's id to Array<Varaint> and send them as packet performance:monitor_id to Editor when custom monitors are added/removed.

Editor Side

  • Editor deserializes Array<Variant> from packet performance:monitor_frame to custom monitor's data.
  • Editor deserializes Array<Variant> from packet performance:monitor_id to custom monitor's ids.
  • CheckBox s are added according to received performance:monitor_id in "Custom" section of existing Monitor tab.
  • Editor draws the graph of custom monitors whose CheckBox is checked.
  • If no data is received in a tick, custom monitor assumes 0 as its value .
  • Negative values are clamped to 0 in line graphs.

Custom Profilers

Profiler UI Scene refers to user-made scene which contains the UI of Custom Profiler. Profiler UI Script is attached to root node

  • Since Godot supports multiple debug sessions simultaneously, therefore add_control_to_dock approach is not applicable for Profiler UI. Instead, EditorDebuggerNode takes a PackedScene, let's call it Profiler UI Scene, of profiler UI and instantiates it to the existing ScriptEditorDebugger's trees. It also adds them to future instances of ScriptEditorDebugger of new debug sessions.

Profiler UI Script refers to user-made script that handles the UI and communication part of Custom Profiler from Editor.

  • The user made Profiler UI Script, attached to the root node of Profiler UI Scene, handles the __Editor __ side of custom profiler through ScriptEditorDebugger

Custom Profiler Node refers to user-made node that handles the task of collecting, processing and sending profiler data from Game to Editor.

  • A user made Custom Profiler Node, attached to main tree of Game through auto-load or other means, handles the Game side of custom profiler through EngineDebugger.

This diagram shows the connections between Custom Profiler Node, Profiler UI Script and Profiler UI Scene during debugging 3 instances of Game with a single Editor.Connection Diagram

​ Above diagram shows the working of Custom Profiler in multiple debug sessions.

  • Profiler UI Script uses message calls and captures with ScriptEditorDebugger to communicate.
  • Custom Profiler Node uses message calls, captures and profiler calls with EngineDebugger singleton to communicate.
  • Profiler calls means the internal interface provided by EngineDebugger::Profiler (toggle, add and tick)
  • Captures are used for routing the incoming messages to specified functions, as done by EngineDebugger::Capture, depending on the starting sub-string of the message. Example- rotation:set is captured by rotation with message set, game:end is captured by game with message end

Custom Profiler API mock-up shows the implementing of a simple profiler that shows device's magnetometer direction.

  • profiler.gd is used for Custom Profiler Node
  • profiler_editor.gd is used for Profiler UI Script. It is attached to root node of Profiler UI Scene.
  • plugin.gd registers Custom Profiler Node and Profiler UI Scene

Implementation

Custom Performance Monitors

Game Side

  • MonitorCall stores the relevant data to call custom monitor function.

    class MonitorCall{
      Callable callable;
      Vector<Variant> arguments;
    };
  • OrderedHashMap<String, MonitorCall> named monitor_map in Performance class maps the ids with respective MonitorCall.

  • Following methods provide interface to add/remove monitors. They add/remove MonitorCall from monitor_map.

    bool add_monitor(const StringName &p_id, const Callable &p_callable, const Vector<Variant> &p_args);
    void remove_monitor(const StringName &p_id)
  • Dirty flag bool ids_dirty in Performance class represents the state of monitor_map

  • ids_dirty is set when monitors are added/removed.

  • RemoteDebugger::PerformanceProfiler::tick method is modified to send performance:monitor_id and performance:monitor_frame

  • If ids_dirty is set, then RemoteDebugger sends ids of monitor_map under packet performance:monitor_id in next RemoteDebugger::PerformanceProfiler::tick.

  • RemoteDebugger calls Callables of monitor_map and stores them in Array<Variant>. It sends the array in packet performance:monitor_data in RemoteDebugger::PerformanceProfiler::tick.

  • performance:monitor_id precedes performance:monitor_data in a tick.

  • Variants are ordered in Array<Variant> in same order as monitor_map

Editor Side

  • MonitorData stores the required data of custom monitor.

    class MonitorData{
      List<float> history;
      float max;
      TreeItem* item;
    };

    history contains the data history of custom monitor.

    max caches the maximum value of history's items.

    item points to the CheckBox of the custom monitor.

  • OrderedHashMap<String,MonitorData> of name monitor_data_map maps ids with MonitorData

  • HashMap<String,int> named monitor_index_map stores the index of custom monitor's data in received data.

  • monitor_data_map and monitor_index_map are member variable of ScriptDebuggerEditor

  • ScriptDebuggerEditor::_parse_message method is modified to process packets named performance:monitor_frame and performance:monitor_id

  • ScriptDebuggerEditor adds new frame to custom monitors while processing performance:monitor_frame

    There are three cases

    • Ids present in monitor_data_map but not in monitor_index_map are of custom monitors which are removed from Game but still checked in Editor
    • Ids present in monitor_data_map and also in monitor_index_map are of custom monitors which are active in Game
    • Ids not present in monitor_data_map but present in monitor_index_map represents the case when performance:monitor_frame is processed before performance:monitor_id. This case will never happen because Game and Editor communicate using RemoteDebuggerPeerTCP which ensures ordering of messages.

    ScriptDebuggerEditor follows following procedure for possible 2 cases

    • 0 is added to history of MonitorData which was mapped with ids in first case.
    • Frame data is added to history of MonitorData which was mapped with ids in second case. index of ids in monitor_index_map is used to get frame data.
  • ScriptDebuggerEditor updates monitor_data_map, monitor_index_map, andTree* perf_monitors while processing performance:monitor_id. The following procedure is followed:

    1. ScriptDebuggerEditor rebuilds monitor_index_map using the received data.
    2. A new OrderedHashMap<String,MonitorData> named monitor_data_map_new is created.
    3. Contents are moved from monitor_data_map to monitor_data_map_new.
    4. Ids which are absent in monitor_index_map and their CheckBox is not checked are dropped in this movement process.
    5. monitor_data_map_new is swapped with monitor_data_map.
    6. Unused ids are removed up to this point.
    7. New custom monitor's ids are added by iterating through received data and checking monitor_map_data
    8. The name of CheckBox is same as id of corresponding custom monitor.
    9. All CheckBox items under Custom TreeNode are removed and rebuilt according to new monitor_data_map
    10. Previously checked items are checked again after rebuild.
  • ScriptEditorDebugger::_performance_draw() method is modified to draw the line graphs of custom monitors.

  • Same drawing procedure is used to draw the line graph with some modifications.

  • ScriptEditorDebugger::start method is modified to clear the history of available MonitorData in monitor_map_data

Custom Profilers

Three classes are exposed to ClassDB in this implementation.

  • EngineDebugger
  • EditorDebuggerNode Partially exposed already.
  • ScriptEditorDebugger Partially exposed already.

EngineDebugger and EditorDebuggerNode are singletons. Their get_singleton methods are registered too.

Functions exposed and their brief implementation overview:

EditorDebuggerNode

It handles register/unregister method of Profiler UI Scene and supplies reference of ScriptEditorDebugger to Profiler UI Script. It is a singleton available in the Editor.

  • func EditorDebuggerNode.register_profiler_scene(id:String, scene:PackedScene)

    It registers scene as Profiler UI Scene with givenid. Current instances of ScriptEditorDebugger instantiate this PackedScene into their tabs. A HashMap<String,Ref<PackedScene>>, let's call it scene_map, is used to store the mapping between id and scene. scene_map is used by EditorDebuggerNode, while adding more debuggers (ScriptEditorDebugger), to instantiate Profiler UI Scene.

    Needs to be implemented

  • func EditorDebuggerNode.unregister_profiler_scene(id:String)

    It removes the entry with given id from the scene_map. Current instances of ScriptEditorDebugger remove the respective instances of Profiler UI Scene from their tabs too.

    Needs to be implemented

  • func EditorDebuggerNode.has_profiler_scene(id:String)->bool

    Returns whether id is in scene_map

    Needs to be implemented

  • func EditorDebuggerNode.get_debugger_id(root_node:Node)->int

    After Profiler UI Scene is instantiated and before it is added to ScriptEditorDebugger's tab tree, an entry is made into a HashMap<ObjectID,int>, let's call it scene_to_debugger_map. The ObjectID of Profiler UI Scene's root node is the key of entry. The id of attached ScriptEditorDebugger is the value of entry. This function uses scene_to_debugger_map to return the id of debugger which contains the scene with root node root_node in it's tabs. If none is found, -1 is returned. Removal of Profiler UI Scene updates the scene_to_debugger_map too.

    Needs to be implemented

  • func EditorDebuggerNode.get_debugger(id:int)->ScriptEditorDebugger

    This function returns the ScriptEditorDebugger of respective id. It's already implemented as EditorDebuggerNode::get_debugger

    Needs to be exposed

EngineDebugger

​ Most functions of Custom Profiler API is already implemented as static functions of EngineDebugger.

EngineDebugger::Profiler and EngineDebugger::Capture are also implemented. I will use them in the API.

  • func EngineDebugger.register_profiler(name:String, toggle:Callable, add:Callable, tick:Callable)

    It wraps EngineDebugger::register_profiler. Three Callable are used instead of a EngineDebugger::Profiler. Function signature of toggle, add, tick are func toggle(state:bool, args:Array), func add(data:Array) and func tick(frame_time:float, idle_time:float, physics_time:float, physics_frame_time:float) respectively.

    Needs to be implemented

  • func EngineDebugger.unregister_profiler(name:String)

    It exposes already implemented EngineDebugger::unregister_profiler

    Needs to be exposed

  • func EngineDebugger.is_profiling(name:String)->bool

    It exposes already implemented EngineDebugger::is_profiling

    Needs to be exposed

  • func EngineDebugger.has_profiler(name:String)->bool

    It exposes already implemented EngineDebugger::has_profiler

    Needs to be exposed

  • func EngineDebugger.register_message_capture(name:String, capture:Callable)

    It wraps EngineDebugger::register_message_capture. Callable is used instead of EngineDebugger::Capture. Function signature of capture is func capture(message:String,args:Array)

    Needs to be implemented

  • func EngineDebugger.unregister_message_capture(name:String)

    It exposes already implemented EngineDebugger::unregister_message_capture

    Needs to be exposed

  • func EngineDebugger.has_capture(name:String)->bool

    It exposes already implemented EngineDebugger::has_capture

    Needs to be exposed

  • func EngineDebugger.profiler_enable(name:String, bool:enable, args:Array)

    It exposes already implemented EngineDebugger::profiler_enable

    Needs to be exposed

  • func EngineDebugger.send_message(message:String, data:Array)

    It exposes EngineDebugger's virtual function send_message

    Needs to be exposed

  • func EngineDebugger.profiler_add_frame_data(name:String, data:Array)

    It exposes already implemented EngineDebugger::profiler_add_frame_data

    Needs to be exposed

ScriptEditorDebugger

  • Unlike EngineDebugger there is no support of message captures in ScriptEditorDebugger. Every message is parsed using a long chain of if/else in ScriptEditorDebugger::_parse_message. A capture system similar to EngineDebugger will be implemented. The system provides a base to decompose the monolithic ScriptEditorDebugger::_parse_message function.

  • Using the capture system, EditorProfiler, EditorVisualProfiler, EditorNetworkProfiler etc can move the parsing code to their respective classes.

  • Just like the EngineDebugger, the capture system will provide following functions:

    • void ScriptEditorDebugger::register_message_capture(const StringName &p_name, Capture p_func)
    • void ScriptEditorDebugger::unregister_message_capture(const StringName &p_name)
    • bool ScriptEditorDebugger::has_capture(const StringName &p_name)
  • Above functions will be exposed using:

    • func ScriptEditorDebugger.register_message_capture(name:String, capture:Callable). The function signature of capture is func capture(message:String,args:Array)

      Needs to be implemented

    • func ScriptEditorDebugger.unregister_message_capture(name:String)

      Needs to be implemented

    • func ScriptEditorDebugger.has_capture(name:String)->bool

      Needs to be implemented

  • ScriptEditorDebugger::_put_msg is used to send messages. An additional function void ScriptEditorDebugger::send_message(const String &p_message, const Array &p_args) is implemented with a Mutex just like RemoteDebugger::send_message.ScriptEditorDebugger::_put_msg is called in it with mutex lock.

  • func ScriptEditorDebugger.send_message(message:String, data:Array) exposes this ScriptEditorDebugger::send_message

    Needs to be implemented

Project Timeline

The project is divided in 5 tasks.

  1. Implementing the Game side of custom performance monitors.
  2. Implementing the Editor side of custom performance monitors.
  3. Implementing the functions for EditorDebuggerNode
  4. Implementing the functions for EngineDebugger
  5. Implementing the functions for ScriptEditorDebugger

Task #1 and #2 are part of custom performance monitors.

Task #3, #4 and #5 are part of custom profilers. These tasks are extra. They need more discussion in community bonding period.

During community bonding period, I will explore Godot Debugger's code base, discuss about standard ways of doing things and discuss more about the custom profiler part.

Period Weeks Activity
May 4 to 1 June NA Community Bonding Period
1 June to 7 June #1 Task #1
8 June to 28 June #2, #3 & #4 Task #2
29 June to 5 July #5 Phase 1 Evaluation. Start Task #3
6 July to 19 July #6 & #7 Task #3_
20 July to 26 July #8 Task #4
27 July to 2 August #9 Phase 2 Evaluation. Start Task #5
3 August to 23 August #10, #11 & #12 Task #5
24 August to 30 August #13 Final Evaluation

Availability

Due to the virus outbreak, there are some chances that my college might shift Summer Vacation for a week or two. So I might not be able to devote 40 hours per week at the start of the program. Besides that, I have no problem with availability.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment