How to tell when a subviewport has finished rendering?

Godot Version

4.7

Question

I want to create a “photobooth” node that can capture images of a model. Basically I want to capture “screen shot” images of a scene independent of my main scene so I can then incorporate those textures back into my main scene. In my original design, I just has a Control that incorporated a subviewport to render them, but I was making so many copies of that control that some users are reporting errors that the graphics system is running out of SAMPLER ids.

ERROR: Cannot create uniform set because there's not enough room in the SAMPLERS descriptors heap.
Please increase the value of the rendering/rendering_device/d3d12/max_sampler_descriptors project setting.
at: (drivers/d3d12/rendering_device_driver_d3d12.cpp:3431)

I was thinking of redesigning this so that instead of creating one subviewport per control, I instead had a global node that had a subviewport attached and I could just send requests to it and it could render the frame I need and then just send the result back. However, I’m not sure how I would be able to tell when it’s finished rendering the frame. I could have my node set up the scene the way I want it in my subviewport and set the subviewport’s render_target_update_mode to UPDATE_ONCE, but then how do I tell when rendering is finished and I can return the subviewport’s texture?

This is the code I have in mind so far:

extends Node
class_name MaterialThumbnailGenerator

@onready var subviewport:SubViewport = %SubViewport
@onready var material_preview_scene:MaterialPreviewScene = %material_preview_scene

class Request extends Resource:
	var material:Material
	var mesh_type:MaterialPreviewScene.MeshType
	var size:Vector2i
	var callback:Callable
	

var queue:Array[Request]
var mutex:Mutex = Mutex.new()

var cur_request:Request

func generate_thumbnail(material:Material, mesh_type:MaterialPreviewScene.MeshType, size:Vector2i, callback:Callable):
	var request:Request = Request.new()
	request.material = material
	request.mesh_type = mesh_type
	request.size = size
	request.callback = callback
	
	mutex.lock()
	queue.push_front(request)
	mutex.unlock()
	
func _process(delta: float) -> void:
	if !cur_request:
		if queue.is_empty():
			return
		
		mutex.lock()

		cur_request = queue.pop_back()
		subviewport.size = cur_request.size
		subviewport.render_target_update_mode = SubViewport.UPDATE_ONCE
		
		material_preview_scene.mesh_type = cur_request.mesh_type
		material_preview_scene.display_material = cur_request.material
		
		mutex.unlock()
		
		#Wait for image to be ready
		#await ???
		
		mutex.lock()
		var img:Image = subviewport.get_texture().get_image()
		var result:ImageTexture = ImageTexture.create_from_image(img)
		cur_request.callback.call(result)
		cur_request = null
		mutex.unlock()

You can use the RenderingServer.frame_post_draw signal to know when a full frame has been rendered.

It’s not possible to wait for just one viewport because Godot internally will re-organize the rendering commands to optimize them.

You’ll need to wait for at least one entire frame to render. So it’s either waiting for RenderingServer.frame_post_draw as mentioned by @mrcdk or get_tree().process_frame