Apparent Race condition using Sprite's _draw: How to solve it ?

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By Alikae

##Project Description

I’m trying to generate a texture via GDScript to use it (as an heightmap) in a shader.

The only way I found to do this was to use a Viewport containing a Sprite, being able to update my ‘texture’ by overwriting Sprite._draw() and to get the result via viewport.get_texture().get_data().

I do my initialization of the Sprite in its first _draw call, here just drawing a black background. Then at every subsequent call I retrieve back the drawn sprite on the viewport, redraw it, and update it by redrawing on top of it.

##Problem

I use a Timer to update the Sprite every .05s, but if I start it in _ready with a wait_time of .05 the first_pass in _draw seems to be discarded, and the black background is not rendered.
My guess would be that the second _draw call is made before the first finish, retrieving a non-initialized image.

Setting the wait_time to .2s the first time seems to solve the problem, but also seems a very bad solution.

My scene tree look like that:
SceneTree

The script inside the Sprite:

extends Sprite


onready var timer = $RedrawTimer


# Needed to avoid Godot freeing them too soon by lack of reference
var tex = null
var old_image = null

# Just used to distinguish the first _draw call,
#     because I need to initialize the texture by code and
#     can't find how to do it outside of _draw.
var first_pass = true


func _ready():
    # There is the problem.
	# Wait .2s at the first pass or the first draw may be discarded (probably a race condition)
	timer.wait_time = 0.2
	timer.connect("timeout", self, '_on_timer_timeout')
	timer.start()


func _on_timer_timeout():
	# Set back the timer cooldown to its normal value
	timer.wait_time = 0.05
	update()


func _draw():
	if (first_pass):
		# Draw black background
		draw_rect(Rect2(Vector2(0, 0), Vector2(200, 200)), Color(0.0, 0.0, 0.0))
		first_pass = false
	else:
		# Take the previous frame image to update it
		old_image = Image.new()
		old_image.copy_from(get_viewport().get_texture().get_data())
		old_image.flip_y() # flip it back. due to the way OpenGL works, the resulting ViewportTexture is flipped vertically.
	
		# Draw it back
		tex = ImageTexture.new()
		tex.create_from_image(old_image)
		draw_texture(tex, Vector2(0, 0))

		# Draw some modifications
		draw_circle(Vector2(50, 70), 10, Color(0.0, 0.0, 1.0, 0.02))

	# Draw a white outline
	draw_rect(Rect2(Vector2(1, 0), Vector2(200, 200)), Color(1.0, 1.0, 1.0), false)

I would like to know if my guess of a race condition is correct, and how to solve it.
Any advice or other method I can use to generate by script a texture usable by shaders would also be welcomed.


Did you tried to to set default texture of sprite to black sprite in the editor? I mean to make black square by yourself as .png image and set it as texture in sprite’s properties? This way, first image should always be black.
You can also use shader’s params (uniforms) to pass data from gdscript to shader. For example, sampler2D can be used for binding 2D textures from gdscript to shader. To set shader’s param from gdscript, you need to use set_shader_param function (ShaderMaterial — Godot Engine (3.4) documentation in English).

AlexTheRegent | 2022-03-18 07:39

As I want to procedurally generate the first image, using a png would mean generate it and it seems hacky and counter productive (Would work nicely with a static first image, though).
I’m already using uniforms, what i’m trying to do is initialize and update properly the texture sent to the GC via set_shader_param.

Alikae | 2022-03-18 16:24

:bust_in_silhouette: Reply From: Zylann

This will sound unintuitive, but you cannot do this in _draw:

    # Take the previous frame image to update it
    old_image = Image.new()
    old_image.copy_from(get_viewport().get_texture().get_data())
    old_image.flip_y() # flip it back. due to the way OpenGL works, the resulting ViewportTexture is flipped vertically.

    # Draw it back
    tex = ImageTexture.new()
    tex.create_from_image(old_image)
    draw_texture(tex, Vector2(0, 0))

The issue is, draw_texture does not immediately draw, it rather puts the call into a queue which will be processed when the rendering engine does its work (which can be in another thread even).
It also does not take ownership of the tex resource. That means as soon as the call to _draw ends, tex is not referenced by anything anymore, and will get deleted. As a result, when the rendering engine processes the drawing queue, it will find a dangling texture.

A solution is to keep your texture alive beyond the _draw function by storing it in a member variable.

Another solution is rather than creating an entire new texture every frame just to flip it, you could use the viewport’s texture directly and flip it with rendering code (negative scale, custom UV, or shader).

See also Can't draw ImageTexture based on generated Image · Issue #24834 · godotengine/godot · GitHub

It also does not take ownership of the tex resource. That means as soon as the call to _draw ends, tex is not referenced by anything anymore, and will get deleted. (…) A solution is to keep your texture alive beyond the _draw function by storing it in a member variable.

I already managed to understand that, at the beginning of the script:
# Needed to avoid Godot freeing them too soon by lack of reference
var tex = null

Another solution is rather than creating an entire new texture every frame just to flip it, you could use the viewport’s texture directly and flip it with rendering code (negative scale, custom UV, or shader).

Right, all the old_image stuff isn’t needed, I’ll remove it, but I still need to _draw.
Do you know if a better way of editing a Texture via code do exist ? I don’t need it anywhere in the sceneTree, after all, it’s all just data to be sent to GC. Using a Viewport for that seems super-weird.

Alikae | 2022-03-18 16:48

A viewport is currently the fastest way to generate a heightmap in my experience, it’s not that weird. It can use the GPU and a shader to perform all per-pixel operations very fast. The only other way without a viewport is to use an Image and call set_pixel in GDScript but that is slow.
Also, if you plan to use the viewport just for generating and never change it afterward, then grabbing the texture is actually a good idea, so you can destroy the viewport when you are done.
I don’t know what you mean by “GC”.

Zylann | 2022-03-18 17:48

Thanks a lot for the answers, I’ll use the viewports way more confidently now !

Actually I update the said texture every few frame (It’s an heightmap deformed by several entities), so removing the viewport isn’t possible.

By the way, it’s a detail but do you know if it’s possible to use this rendering ability of viewports without them being in the sceneTree or visible ? Right now i’ve just moved them out of the screen so it’s not that much of a problem, but again, it seems weird to not be able to do it.

‘GC’ was for Graphic Card, sorry, forgot it wasn’t that commonly used.

Alikae | 2022-03-18 22:04

In theory you could use viewports without nodes by creating them using VisualServer directly, however I tried doing that once and had an API issue (something like a function returning a RID but another function expecting a ViewportTexture and the impossibility to create it from a RID, I don’t remember precisely), so I had to rollback to the Viewport node.

Zylann | 2022-03-18 22:32