godotengine / godot

Godot Engine – Multi-platform 2D and 3D game engine
https://godotengine.org
MIT License
91.49k stars 21.26k forks source link

OpenGL: Shader compilation stutter when rendering a 2D element for the first time #76241

Open metajarra opened 1 year ago

metajarra commented 1 year ago

Godot version

4.0.2.stable.mono.official (7a0977ce2)

System information

Windows 11, Compatability, Intel - Intel(R) Iris(R) Xe Graphics

Issue description

I'm using Godot Mono version v4.0.2.stable.mono.official.7a0977ce2

When instantiating a Control node from a PackedScene for the first time, there is a noticeable lag and drop in framerates (60fps to 23fps on my computer). After the first instantiation, this issue no longer exists.

This only occurs in the Compatibility renderer and not in the Mobile/Forward+ renderers. This was tested on a friend's computer using Windows 10 and the same issue occurred in Compatibility only. This was also tested using standard Godot (rather than Godot Mono and this issue also occurred in Compatibility only.

I tested this using Buttons and Labels so I can only confirm that this exists for these Control nodes, however it's possible it occurs for other Control nodes as well.

Steps to reproduce

The project is trivial so a reproduction project is not included.

I used the following code:

// C#
using Godot;
using System;

public partial class instantiation_lag_demo : HBoxContainer
{
    private PackedScene buttonPrefab;

    public override void _Ready(){
        buttonPrefab = GD.Load<PackedScene>("res://button.tscn");
    }

    public override void _Process(double delta){
        if (Input.IsActionJustPressed("click")){
            Button button = (Button)buttonPrefab.Instantiate();
            AddChild(button);
        }
    }
}
#GDScript
extends HBoxContainer
@onready var button_scene = preload("res://button.tscn")

# Called when the node enters the scene tree for the first time.
func _ready():
    pass # Replace with function body.

# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
    if Input.is_action_just_pressed("click"):
        var new_guy = button_scene.instantiate()
        add_child(new_guy)

This script extends HBoxContainer and is attached to an HBoxContainer in the scene tree. It loads a PackedScene "button.tscn", a scene made up of a single Button with Layout/Container Sizing/Horizontal Expand set to true (this issue is also reproduced if this is set to false).

A single input action is setup called "click" which is triggered when the left mouse button is pressed.

To observe this issue, run the project and click once. The first button will take some time to load and produce a noticeable drop in framerates - clicking again will create additional buttons which load in far less time (ie. no lag, no noticeable drops in framerates).

Minimal reproduction project

N/A

Calinou commented 1 year ago

This may be due to shader compilation stutter (to draw the button texture).

Can you reproduce this in GDScript?

metajarra commented 1 year ago

This may be due to shader compilation stutter (to draw the button texture).

Can you reproduce this in GDScript?

Yes it's reproducible in GDScript using the following code:

#GDScript
extends HBoxContainer
@onready var button_scene = preload("res://button.tscn")

# Called when the node enters the scene tree for the first time.
func _ready():
    pass # Replace with function body.

# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
    if Input.is_action_just_pressed("click"):
        var new_guy = button_scene.instantiate()
        add_child(new_guy)

This causes the same error in both standard (ie. non-.NET) Godot and in my install of Godot Mono

Calinou commented 1 year ago

Can you reproduce this issue if you add a Button node in the main scene (so that it's visible as soon as you run the project)? I guess this is an issue that most people don't notice because they have at least one 2D element visible on the first place (either a label or button).

metajarra commented 1 year ago

Can you reproduce this issue if you add a Button node in the main scene (so that it's visible as soon as you run the project)? I guess this is an issue that most people don't notice because they have at least one 2D element visible on the first place (either a label or button).

Ok testing with a Button already being visible seems to solve the issue. Please let me know if there's any other known solution since this seems janky

Calinou commented 1 year ago

Please let me know if there's any other known solution since this seems janky

Reduce the button's opacity to a very low but non-zero value using the Modulate property, and use queue_free() on that button after one frame has passed:

extends Button

func _process(delta):
    call_deferred("queue_free")

You can also scale down the node significantly or put it behind other 2D nodes (such as a ColorRect), but again, the scale should be greater than 0.

Regarding jank: Both 2D and 3D games often end up having to do this to minimize shader compilation stutter during gameplay, even AAA productions. It's a hard problem to fully resolve, as we can't predict all shader combinations that a project might use in advance.

Work is being done on adding shader caching to 4.x's Compatibility rendering method in https://github.com/godotengine/godot/pull/76092, which will alleviate this issue after starting the project once. It may also be possible to force some basic 2D shaders to always compile in a separate pull request.

metajarra commented 1 year ago

Both 2D and 3D games often end up having to do this to minimize shader compilation stutter during gameplay, even AAA productions. It's a hard problem to fully resolve, as we can't predict all shader combinations that a project might use in advance.

Work is being done on adding shader caching to 4.x's Compatibility rendering method in https://github.com/godotengine/godot/pull/76092, which will alleviate this issue after starting the project once. It may also be possible to force some basic 2D shaders to always compile in a separate pull request

Great to know, thank you! I appreciate your help

clayjohn commented 1 year ago

It would be helpful to know if this issue still exists in Godot 4.1. We added shader caching, so the issue should be mostly resolved if the issue is caused by shader compilation stutter

djrain commented 1 year ago

It would be helpful to know if this issue still exists in Godot 4.1. We added shader caching, so the issue should be mostly resolved if the issue is caused by shader compilation stutter

Encountering a similar problem in 4.1.1 - tried switching our game from Mobile to Compatibility, and suddenly instantiating / showing things for the first time causes massive stutters on our Pixel 4. I can't seem reproduce the issue with buttons or labels, but loading 10 unique custom shaders on Mobile mode is instant, whereas on Compat it took an entire 5 seconds. I do have shader cache enabled in settings. That is very odd, no?