How Godot SceneTree Initialization and the Main Loop Work: A Deep Dive into the Engine Source

Godot's SceneTree initialization begins in Main::start() which creates a MainLoop instance (typically SceneTree) and registers it with the OS singleton; the platform-specific OS run-loop then drives the engine by calling physics_process() for fixed-step physics and process() for frame updates until the application quits.

The SceneTree serves as the central nervous system of every Godot application, managing the node hierarchy,viewport rendering, and the execution flow that powers both gameplay and editor functionality. Understanding how the godotengine/godot repository initializes this system and maintains its main loop is essential for engine contributors and advanced users building custom runtime behavior. This article examines the exact source code paths that bootstrap the SceneTree and the architectural patterns that govern the per-frame execution cycle.

The Startup Sequence: From Main to SceneTree

The engine boot process follows a strict three-phase initialization defined in main/main.cpp. This sequence establishes the foundation upon which the SceneTree is constructed and activated.

Entry Points in main/main.cpp

Execution begins with static methods in the Main class. First, Main::setup() parses command-line arguments and initializes low-level subsystems. Next, Main::setup2() completes resource loading and server initialization. Finally, Main::start() determines which MainLoop implementation to instantiate based on the execution context (editor, project manager, or standalone game).

During Main::start(), the engine evaluates whether to launch the editor or runtime:

// main/main.cpp (simplified)
MainLoop *main_loop = nullptr;
if (editor) {                     // --editor flag or project manager
    main_loop = memnew(SceneTree);   // default scene-tree implementation
}
...
OS::get_singleton()->set_main_loop(main_loop);   // registers it with OS

The memnew(SceneTree) allocation creates the concrete MainLoop object, while OS::set_main_loop() stores this pointer within the OS singleton for later retrieval by the platform layer.

Registering with the OS Layer

The OS abstraction (core/os/os.h) acts as the bridge between platform-specific code and engine logic. When set_main_loop() receives the SceneTree pointer, it prepares the system to delegate lifecycle events to this object. This registration must complete before OS::run() begins, as the run-loop assumes a valid MainLoop instance exists.

The MainLoop Interface and OS Abstraction

Godot decouples the platform run-loop from game logic through the abstract MainLoop class defined in core/os/main_loop.h. This interface declares the virtual methods that every main loop implementation—including SceneTree—must override.

The MainLoop Virtual Interface

The MainLoop base class establishes four critical lifecycle hooks:

class MainLoop : public Object {
    GDCLASS(MainLoop, Object);
protected:
    static void _bind_methods();

    GDVIRTUAL0(_initialize)
    GDVIRTUAL1R(bool, _physics_process, double)   // physics tick
    GDVIRTUAL1R(bool, _process, double)          // per-frame tick
    GDVIRTUAL0(_finalize)

public:
    virtual void initialize();       // called once at startup
    virtual bool physics_process(double p_time);  // fixed timestep
    virtual bool process(double p_time);          // variable frame time
    virtual void finalize();        // called on shutdown
    virtual ~MainLoop() {}
};

The boolean return values from physics_process() and process() serve as exit signals; returning true requests immediate engine termination.

The OS Run-Loop Implementation

Platform-specific OS classes (such as OS_Unix or OS_Windows) implement the run() method declared in core/os/os.h. This method contains the actual while-loop that drives the engine until _quit becomes true:

// core/os/os.cpp (conceptual)
int OS::run() {
    while (!_quit) {
        // handle OS events (window messages, input, etc.)
        
        if (main_loop) {
            // fixed-step physics
            if (main_loop->physics_process(delta)) break;
            
            // idle processing and rendering
            if (main_loop->process(delta)) break;
        }
    }
    return exit_code;
}

This architecture ensures that physics processing executes at a fixed timestep independent of frame rate, while process ticks occur as frequently as the display refresh allows.

SceneTree Implementation Details

SceneTree (scene/main/scene_tree.h and scene/main/scene_tree.cpp) inherits from MainLoop and provides the concrete implementation used by default in Godot projects. It extends the basic interface with node management, group processing, and signal emission.

Initialization Phase

When OS::run() first invokes the main loop, it calls SceneTree::initialize(). This method performs several critical setup operations:

  • Configures the root Viewport and Window nodes
  • Registers the tree with the Engine singleton
  • Establishes default input handling and rendering contexts
  • Emits the "tree_changed" signal to notify listeners that the hierarchy is ready

This initialization occurs exactly once per application lifetime, before any physics or rendering occurs.

Physics and Process Ticks

The SceneTree implements the dual-update strategy required for stable game simulation:

physics_process(double p_time) executes at a fixed physics tick rate (default 60 Hz). It processes:

  • Physics server synchronization
  • Timer and tween updates tied to physics
  • Node groups registered for physics callbacks
  • Emission of the "physics_frame" signal

process(double p_time) runs once per rendered frame. It handles:

  • Idle-time node group processing
  • Input event propagation
  • Rendering server updates and draw calls
  • Emission of the "process_frame" signal

Both methods return false by default to continue execution, or true to trigger immediate shutdown.

Frame Lifecycle Hooks

Between the physics and process phases, SceneTree utilizes iteration_prepare() and iteration_end() (declared in scene/main/scene_tree.h) to manage interpolation states and synchronization points. These hooks enable features like physics interpolation and ensure the rendering server receives consistent data during multi-threaded scenarios.

Practical Code Examples

Creating a Custom MainLoop in C++

You can bypass SceneTree entirely by implementing a custom MainLoop subclass:

class MyLoop : public MainLoop {
    GDCLASS(MyLoop, MainLoop);
public:
    void initialize() override {
        print_line("MyLoop init");
    }
    
    bool physics_process(double delta) override {
        // Fixed-step physics code here
        return false; // return true to request quit
    }
    
    bool process(double delta) override {
        // Per-frame rendering logic here
        return false;
    }
    
    void finalize() override {
        print_line("MyLoop cleanup");
    }
};

// Registration:
OS::get_singleton()->set_main_loop(memnew(MyLoop));

Accessing the SceneTree in GDScript

From any node in the hierarchy, access the active SceneTree using:

var tree = get_tree()                # Returns the current SceneTree

print(tree.get_frame())              # Current frame counter

tree.quit()                          # Request engine shutdown

Connecting to Engine Signals

Monitor structural changes and frame events:

func _ready():
    get_tree().connect("tree_changed", self, "_on_tree_changed")
    get_tree().connect("physics_frame", self, "_on_physics_frame")

func _on_tree_changed():
    print("SceneTree structure updated")
    
func _on_physics_frame():
    print("Physics tick occurred")

Summary

  • Initialization Flow: Godot bootstraps through Main::setup()Main::setup2()Main::start(), which instantiates SceneTree and registers it via OS::set_main_loop() in main/main.cpp.
  • Abstraction Layer: The MainLoop interface in core/os/main_loop.h decouples platform code from game logic, declaring initialize(), physics_process(), process(), and finalize() hooks.
  • Execution Loop: Platform-specific OS implementations call the virtual methods in a tight loop, separating fixed-timestep physics from variable-time rendering.
  • SceneTree Extensions: scene/main/scene_tree.cpp adds node management, group processing, and viewport rendering atop the base interface.
  • Customization: Developers can replace the default SceneTree with custom MainLoop implementations by subclassing and registering with the OS singleton.

Frequently Asked Questions

What is the difference between MainLoop and SceneTree?

MainLoop is an abstract interface defined in core/os/main_loop.h that specifies the minimum contract required to drive the engine—initialization, physics processing, frame processing, and finalization. SceneTree is a concrete implementation located in scene/main/scene_tree.h that inherits from MainLoop and adds node hierarchy management, viewport handling, group processing, and GDScript signal emission. While all Godot games use a MainLoop, most use the SceneTree specialization unless implementing a custom runtime that does not require nodes.

How do I access the SceneTree from a Node?

Call get_tree() from any class inheriting from Node. This method returns a pointer to the active SceneTree instance registered with the OS singleton. From GDScript, this provides access to methods like change_scene_to_file(), create_timer(), and quit(). In C++ module development, you can also retrieve it via SceneTree::get_singleton() after initialization completes.

Can I replace the default SceneTree with a custom MainLoop?

Yes. Create a C++ class inheriting from MainLoop, override the virtual methods (initialize(), physics_process(), process(), finalize()), and register it with OS::get_singleton()->set_main_loop() before OS::run() begins. This technique is used for specialized applications like headless servers, custom rendering engines, or non-game tools built on Godot's core infrastructure that do not require the node system.

How does the engine handle physics vs. process timing?

The physics_process() method executes at a fixed timestep defined by ProjectSettings.physics/common/physics_ticks_per_second (default 60 Hz), ensuring deterministic simulation regardless of frame rate. The process() method executes once per rendered frame, passing the actual delta time since the previous frame. The OS run-loop in core/os/os.cpp calls both methods sequentially each iteration, allowing SceneTree to update physics servers before processing input and rendering.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →