godotengine / godot

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

Compile shaders when a scene is loaded #13954

Closed endragor closed 3 years ago

endragor commented 7 years ago

Operating system or device, Godot version, GPU Model and driver (if graphics related): Godot af27414b1c10173584539186e396668a899e06b2, any OS

Issue description:

Currently shaders are compiled lazily - whenever a shader needs to be invoked, glCompileShader is called. This results in noticeable freezes in-game when some object is rendered for the first time. Behaviour that would make more sense is to compile the shader when a scene depending on it is loaded.

mpue commented 5 years ago

Another thing is: The shaders are being compiled on demand in the editor too. This causes the editor to lag on scenes with lots of shaders.

mpue commented 5 years ago

Ok so I can confirm, that this problem does not come from the shader compile which happens right before the scene becomes visible. The shaders are also correctly being cached.

Something I've noticed: Another problem comes from the GIProbe, which makes the stutter at the beginning even worse. Probably there is some light caching problem here?

eon-s commented 5 years ago

@mpue for GI try on release builds, or save GI data in an external .res (not .tres) file, if the issue disappears is then something else related to loading the baked information.

starry-abyss commented 5 years ago

I can confirm, that this problem does not come from the shader compile

There are different shaders to compile per different material (https://www.youtube.com/watch?v=Cg4ZT6X0ghs). After they are compiled, they are cached by the driver, and next time are compiled fast. But if you erase the cache (https://github.com/godotengine/godot/issues/13954#issuecomment-453267114), the problem returns.

owstetra commented 5 years ago

Bump , Any news or updates ?

starry-abyss commented 5 years ago

Reduz mentioned in twitter that it's fixed in Godot 4.0 branch. Apparently won't be fixed in GLES 3, because this renderer is going to be removed.

ghost commented 5 years ago

@starry-abyss My development efforts are invested in 3.1. Switching to 4.0 at some unknown future date to fix CPUParticles2D from stuttering the game will not be an option available to us. So I hope there is still interest in supporting previous versions.

eon-s commented 5 years ago

@avencherus if you have problems with CPU particles, then it is another issue, GPU particles suffer from loading shaders but CPU ones should not and solving this problem may not help.

starry-abyss commented 5 years ago

@avencherus Do you experience the issue in GLES 2?

ghost commented 5 years ago

@starry-abyss It's in GLES3, haven't tried GLES2 yet. Have to add them to the SceneTree during loading, and move them out of view. Then keep them in the scene.

@eon-s Wasn't your issue closed, citing this was the cause?

eon-s commented 5 years ago

No, that was about regular particles, not CPU ones, the problem was on the particles shader compilation like this issue.

Megalomaniak commented 5 years ago

Wouldn't cpu particles using a particle material/shader still potentially be able to run into some issue with this? Mind I haven't used cpu particles, but I assume they can still be shaded with custom shaders, no?

clayjohn commented 5 years ago

@Megalomaniak The issue with Particles is not the drawing material but the process material. Particles have to compile two shaders (drawing and process), which doubles the length of the hiccup. CPUParticles only have the drawing material.

Additionally, if using a SpatialMaterial, it is likely the shader is compiled already, so drawing materials often have no hiccup at all.

ghost commented 5 years ago

It will be a few months before I'm doing any sort of visual effects work again, but from memory of what I was doing when I encountered it last, I was using a CanvasMaterial for additive blend mode on CPU Particles. That may be the source of it, but unfortunately no time for me to investigate it any deeper.

When or if I bump into it again, I'll see if I can put it in a minimal project and post it.

girng commented 5 years ago

One important aspect I didn't see in this issue is: preload. When preload is used, the developer is explicitly setting their resource to be preloaded. Thus, it shouldn't cause freezing when added to the scene tree (unless it's some huge 900GB scene or something). It should be smooth as butter on any version of Godot.

This is vital for multiplayer games. Example: Player A joins a game and uses a skill effect. It should not cause Player B to freeze the first time the skill is used.

I stumbled upon this issue because I am experiencing this as well. I have a bobble effect and glow shader for dropped items. When a player drops an item for the first time, there is a small freeze (very annoying!). I'm crafting a solution right now and I might switch to using AnimatioinPlayer for the bobble and remove the glow effect. Or, preload the scene so it's in the player's viewport but somehow hidden (so the initial freeze doesn't happen). Still a WIP, but good lord these are nasty bugs.

Or more precisely, maybe compile the shader when its resource is loaded, which is actually what would be expected?

Yes!! 👍

girng commented 5 years ago

@endragor helped a lot on Discord. If anyone stumbles upon this issue, you came to the darkness, for knowledge! (WARNING: this solution works for me. I'm an eccentric programmer so please be wary of my code):

var smooth_preloads = [] func _ready(): smooth_preloads.push_back("res://Scenes/ItemTemplate.tscn") smooth_preload()

func smooth_preload(): for path in smooth_preloads: var t = load(path).instance() add_child(t) yield(get_tree(), "idle_frame")


- Set your `CanvasLayer`'s `layer` property to `-1`! <-- Where the magic happens

I tried a multitude of different ways. From hiding the node, setting the z to -1, and  changing the modulate/opacity to 0.  Enjoy, and long live Godot!

edit: Holy crap, I'm so hyped now, skill effects/scenes with shaders don't lag at all. This is epic
eon-s commented 5 years ago

@girng comment about the workaround on documentation repository, so it can be added somewhere in the docs

LinuxUserGD commented 5 years ago

Not sure if this is a good workaround as add_child() gets called inside of smooth_preload(). It also depends on the visibility of the object. If it's outside of the camera view, it won't work.

fracteed commented 5 years ago

@LinuxUserGD I was also under the impression that a shader won't compile unless it is visible and in the camera frustum (ie actually drawn). I have tried all the workarounds, but they have never really worked reliably.

@girng can you explain why this method works, as I don't really understand what you are doing there. Are you waiting for the first idle frame to instance certain objects with shaders? How would this work if you had hundreds of spatial materials to compile?

@endragor would be great if you could detail your workaround here :)

girng commented 4 years ago

It also depends on the visibility of the object. If it's outside of the camera view, it won't work.

This is why the smooth preloaded scenes must be appended to a CanvasLayer node :D. It's viewable regardless of a camera.

@fracteed Not sure LOL. Just trial and error. I thought it wouldn't work because it's invisible on the viewport (layer is -1). However, I guess internally to Godot it's not, thus negating the freeze. I also yield the idle_frame for a smoother startup (quality of life thing I do if you load a lot of scenes, not required, sorry for the confusion!)

With that said, this is such a weird issue. If you add a preloaded scene with shader code, it will cause a freeze (if you don't use my smooth preloaded solution). However, if you decide to instance that scene again even when it's not visible on the screen.. there is no freezing. My solution is for the first time the scene is being instanced... wtf preload, why are you doing us dirty like this?

edit: I wonder if preload can be modified to prevent the initial loading freeze with shaders/materials. I'm sure the lead devs can craft a solution for this

edit2: Oh god, this is from 2017. @RandomShaper might be able to help

Calinou commented 4 years ago

edit: I wonder if preload can be modified to prevent the initial loading freeze with shaders/materials. I'm sure the lead devs can craft a solution for this

This should be solved in the vulkan branch, as all shaders are compiled on load and cached. I don't know how it'll work with custom shaders though.

LinuxUserGD commented 4 years ago

Does this also work for GLES2 (vulkan branch)? Would be nice if it could be somehow cherry-picked for master and GLES3 then.

Calinou commented 4 years ago

@LinuxUserGD The GLES2 renderer currently doesn't work in the vulkan branch. It needs to be heavily refactored before it can work there.

Also, the current GLES2 and GLES3 renderers are considered to be in maintenance mode. Last time I heard, reduz said it wasn't possible to add shader precompilation to the current renderers, and I don't think it'd be possible without spending a lot of resources on it.

girng commented 4 years ago

I just read @avencherus's concerns. I believe there are more devs in that same predicament. A lot of their development has been done on a specific renderer, thus, if a temporary solution could be added to hold them over until Vulkan, it would be ideal. Otherwise, it's every developer for themselves, until they stumble upon this issue and use a hack.

I personally think it's just a bug with preload, but as reduz said, "You don't know what to compile until you render it". So maybe when preload is used, the shader material can be rendered behind the scenes and stored somewhere. Similar to what @endragor's hack does, or mine. At least give the developer the option to enable this so preload instancing is smooth. I say option, as it can be opt-in, so it doesn't break compat.

Calinou commented 4 years ago

@girng It'd be a good start to provide a reusable asset on the asset library :slightly_smiling_face:

endragor commented 4 years ago

@fracteed for 3D the shader cache node should add the camera child with the same environment parameters as the camera you use in the game and ensure it points to (0,0,0). Then just make the camera active and let the node process() for 1 frame, then hide the node, then switch from the loading screen to your level, but do not remove the shader cache node or else the shaders could be loaded out of memory. Canvas is rendered on top of 3D, so even if there is junk in 3D during loading screen, the user won't see it.

And, as noted above, this logic can be moved into an independent asset that works for both 2D and 3D, but that would require testing for all the conditionals used in shader compilation.

starry-abyss commented 4 years ago

@LinuxUserGD Does the issue happen for you in GLES 2? Or do you ask just in case? Because I only could reproduce in GLES 3 (https://github.com/godotengine/godot/issues/13954#issuecomment-453267114), but I don't have first-person projects, was using a repro from neighbour issue.

fracteed commented 4 years ago

@endragor thanks for the info! I had a feeling that shaders were being unloaded, as that would explain why I was running into some issues. I guess you have this shader cache node scene in an autoload?

How are you ensuring particle systems compile, as I am fairly sure that the particle system needs to emit visibly before the process material is compiled? I guess just emitting for 1 frame in process() should suffice.

I also only recently discovered that the shaders had to be in process for 1 frame, as I had been trying to do this in ready(). With all this new info, I will try to get this working when I get a chance. I am most likely moving my project to Vulkan, but it would still be useful to get this working in 3.2!

Also thanks @girng for your input.

eon-s commented 4 years ago

@fracteed have you tried preloading a particles material on another emitter, without visible meshes? Also drawing all on a 1x1 pixel ViewportTexture may force to compile some shaders.

fracteed commented 4 years ago

@eon-s the problem with particle systems is that 2 shaders need to be compiled, the process material and the material on the particle itself. I am not sure what ensures the particle process material gets compiled. Maybe just emitting 1 frame in process() will suffice, but does it need to be visible to the camera?....these hacks are all a bit of a dark art :)

eon-s commented 4 years ago

These hacks look like dark art but other engines of this kind have options that seems to be doing exactly that for the user.

endragor commented 4 years ago

@fracteed In our case the shader cache node is just part of the scene tree added during loading screen, it's not in autoload. For Particles we create a node with amount = 1, draw_passes and process_material copied from the original node and let it emit for 1 frame while being visible, like everything else.

girng commented 4 years ago

@fracteed. After further review and discussion from an employee at a AAA studio on Discord, and help from @TheDuriel; apparently you don't even need to load the scene, which nullifies my solution. I was just trying to get something to work. This is why I am truly a Hopeless developer.

I stumbled upon this reddit post and someone posted what looks like a far better solution. So maybe that should make its way into the asset library instead.

Zireael07 commented 4 years ago

I believe most of the solutions are for 2D, and the problem is also in 3D - even more apparent there...

TheDuriel commented 4 years ago

you do need to load and display the shader. past that, it does not matter. yes you can hide it behind a loading screen. @girng

@Zireael07 there is no difference between 2d and 3d here.

RockyMadio commented 4 years ago

@endragor helped a lot on Discord. If anyone stumbles upon this issue, you came to the darkness, for knowledge! (WARNING: this solution works for me. I'm an eccentric programmer so please be wary of my code):

  • Create a CanvasLayer, attach the following script:
extends CanvasLayer

var smooth_preloads = []
func _ready():
  smooth_preloads.push_back("res://Scenes/ItemTemplate.tscn")
  smooth_preload()

func smooth_preload():
  for path in smooth_preloads:
      var t = load(path).instance()
      add_child(t)
      yield(get_tree(), "idle_frame")
  • Set your CanvasLayer's layer property to -1! <-- Where the magic happens

I tried a multitude of different ways. From hiding the node, setting the z to -1, and changing the modulate/opacity to 0. Enjoy, and long live Godot!

edit: Holy crap, I'm so hyped now, skill effects/scenes with shaders don't lag at all. This is epic

I tested your method. It works perfectly fine. After adding dummy scenes to precompile the shaders, you can leave them on the screen for a few seconds then hide them(children) away, then emit an signal to let the "blackscreen" fade out.

TheDuriel commented 4 years ago

You dont need to keep the nodes. just the material and thus shader resource.

snoopdouglas commented 4 years ago

I'm finding that with both the above solution and the Reddit thread, there are still stutters when my precompilation scene isn't run directly before the scene in which the shaders are used. I presume this is because they're discarded from some cache.

@Calinou linked this video in the above mention, which presents a solution that'll solve that: keep all of the shaded objects in the scene and set them to invisible after one frame. Sadly, this won't suit my workflow, so I'm just going to look forward for a proper fix for this in 4.0.

jitspoe commented 4 years ago

Could we not create a second OpenGL context in a second thread and use that to compile the shaders? That way we could pre-load all the materials and have a loading screen or something without hitches. I'm trying to loop through and draw all my materials (one every frame or 2) at the game start, and this can take a long time on some hardware, freezing for seconds at a time.

clayjohn commented 4 years ago

@jitspoe unfortunately no, OpenGl contexts cannot share resources. This was one of the big limitations that the Vulkan API was created to overcome. In Vulkan you can do just that, compile shaders in one thread and then use them in another once they are ready (for when you can't preload them) unfortunately it is not possible in OpenGL.

jitspoe commented 4 years ago

Hmm, I've seen a number of suggestions online to do exactly that. Though I've also read that this approach isn't useful because some drivers freeze all contexts while waiting for a shader to compile. This seems like... wow. Like what did they expect games to do? Just stutter like crazy or just have crazy load times that completely freeze with no feedback to compile all possible shaders?

Calinou commented 4 years ago

This seems like... wow. Like what did they expect games to do? Just stutter like crazy or just have crazy load times that completely freeze with no feedback to compile all possible shaders?

Steam games can bundle cached shaders, but from what I know, these must be compiled individually on each GPU model. (Compiled shaders aren't compatible across GPU models.)

akien-mga commented 4 years ago

Like what did they expect games to do? Just stutter like crazy or just have crazy load times that completely freeze with no feedback to compile all possible shaders?

I would expect that many games with loading screens actually freeze rendering while compiling shaders incrementally:

snoopdouglas commented 4 years ago

I've always thought this was a bit out of its remit, but Steam caches shader binaries for even faster loading.

Zireael07 commented 4 years ago

@akien-mga: I think some games do that (e.g. FUEL). Any way to imitate such a loading screen in Godot?

snoopdouglas commented 4 years ago

@Zireael07 I guess you'd want to follow the video's instructions, but make each material visible in turn as part of your loading screen.

Zireael07 commented 4 years ago

@snoopdouglas: Thing is, FUEL's loading screen is just a bar on some static 2d background, the materials aren't visible to the end user.

snoopdouglas commented 4 years ago

@Zireael07 Did you watch the video? They accomplished it by adding less-than-1x1 pixel meshes in front of the camera. Each one had a different material. They aren't visible so far as the player's concerned.

More to the point, though, is that this is a workaround for a Godot-specific problem. In other engines, I have more fine-grained control over when the shaders are compiled, so I don't have to resort to janky solutions like that.

eon-s commented 4 years ago

I have seen it on some Unity mobile games, you can actually see the dot where materials are placed on screen if your device is sufficiently bad...

If any of these solutions mentioned in the last 3 years is generic enough, maybe it could be considered for adding to the engine? (export options, perhaps?)

remram44 commented 4 years ago

I was dealing with this today, because I exported a game to HTML5, where the pauses are most noticeable. Having this pre-loading is fine, however I'm not sure I understand how it works. How can I prevent the compiled shared to be thrown away and recompiled? Do I need to keep the reference to load("res://xxx.tscn") (PackedScene) alive? Is the cache shared between all scenes loaded from the same file, or do I need to pass that reference around? Is just the material sufficient? Is showing those scenes for 1 frame enough or do you need to wait until some frames actually get rendered on screen (which on web can take long)?