crash with `USER ERROR: Condition "!_fp" is true.`

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

I’m getting a crash that is telling me I have a user error, but the message doesn’t point to a line in my user code.

I’m trying to offload some expensive computations to a thread, so I suspect that is the problem, but this error is not quite enough to help me figure out what to try next, so I’m hoping maybe someone has seen this before and might have some general suggestions.

Here is the error:

USER ERROR: Condition "!_fp" is true.
   at: _ref (core/variant/array.cpp:55)
USER ERROR: Condition "!_fp" is true.
   at: _ref (core/variant/array.cpp:55)
USER ERROR: Condition "!_fp" is true.
   at: _ref (core/variant/array.cpp:55)
USER ERROR: Condition "!_fp" is true.
   at: _ref (core/variant/array.cpp:55)

================================================================
handle_crash: Program crashed with signal 11
Engine version: Godot Engine v4.0.2.stable.official (7a0977ce2c558fe6219f0a14f8bd4d05aea8f019)
Dumping the backtrace. Please include this when reporting the bug to the project developer.
[1] /lib/x86_64-linux-gnu/libc.so.6(+0x43090) [0x7fb7dc1cd090] (??:0)
[2] ~/Desktop/Godot_v4.0.2-stable_linux.x86_64() [0x44ff5cd] (??:0)
[3] ~/Desktop/Godot_v4.0.2-stable_linux.x86_64() [0x457ff66] (??:0)
[4] ~/Desktop/Godot_v4.0.2-stable_linux.x86_64() [0x45a14ad] (??:0)
[5] ~/Desktop/Godot_v4.0.2-stable_linux.x86_64() [0x129f236] (??:0)
[6] ~/Desktop/Godot_v4.0.2-stable_linux.x86_64() [0x1192e1f] (??:0)
[7] ~/Desktop/Godot_v4.0.2-stable_linux.x86_64() [0x459ca1b] (??:0)
[8] ~/Desktop/Godot_v4.0.2-stable_linux.x86_64() [0x43ea2ca] (??:0)
[9] ~/Desktop/Godot_v4.0.2-stable_linux.x86_64() [0x1294354] (??:0)
[10] ~/Desktop/Godot_v4.0.2-stable_linux.x86_64() [0x1192e1f] (??:0)
[11] ~/Desktop/Godot_v4.0.2-stable_linux.x86_64() [0x2b67e0d] (??:0)
[12] ~/Desktop/Godot_v4.0.2-stable_linux.x86_64() [0x3155a1a] (??:0)
[13] ~/Desktop/Godot_v4.0.2-stable_linux.x86_64() [0x45788a4] (??:0)
[14] ~/Desktop/Godot_v4.0.2-stable_linux.x86_64() [0x2ba518e] (??:0)
[15] ~/Desktop/Godot_v4.0.2-stable_linux.x86_64() [0x2bdc9e4] (??:0)
[16] ~/Desktop/Godot_v4.0.2-stable_linux.x86_64() [0xec77c6] (??:0)
[17] ~/Desktop/Godot_v4.0.2-stable_linux.x86_64() [0xe098b3] (??:0)
[18] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf3) [0x7fb7dc1ae083] (??:0)
[19] ~/Desktop/Godot_v4.0.2-stable_linux.x86_64() [0xe29b3e] (??:0)
-- END OF BACKTRACE --
================================================================

and here is the suspicious code:

extends Node2D

class_name NavVectorField

var level_data:LevelData 
@export var update_interval: float = 1.0

var time_since_update: float = 0.0

var vec_field_MAIN: Array = []  # Main thread's copy of the vector field
var target:Player

var update_thread = Thread.new()
var update_mutex = Mutex.new()
var stop_thread = false

func initialize_filled_field(rows: int, cols: int, fill_val) -> Array:
	var field = Array()
	field.resize(rows)
	for i in range(rows):
			field[i] = Array()
			field[i].resize(cols)
			field[i].fill(fill_val)
	return field

func init_nav_grids_and_target_ref(level:LevelData) -> void:
	level_data = level
	target = level_data.player
	var rows = level_data.grid.size()
	var cols = level_data.grid[0].size()
	vec_field_MAIN.resize(rows)
	for i in range(rows):
			vec_field_MAIN[i] = Array()
			vec_field_MAIN[i].resize(cols)
			vec_field_MAIN[i].fill(Vector2(0, 0))

func get_gradient_at_world_position(world_position: Vector2) -> Vector2:
	var tile_size = level_data.TILE_SIZE
	var grid_position = (world_position / tile_size).floor()

	if not is_valid(grid_position, level_data.grid):
			print("invalid grid position: ", grid_position)
			return Vector2(0, 0)
	var grad = vec_field_MAIN[grid_position.y][grid_position.x]  # Use vec_field_MAIN instead of vec_field_WORKER
	return grad

func _exit_tree() -> void:
	update_mutex.lock()
	stop_thread = true
	update_mutex.unlock()
	if update_thread.is_alive():
		update_thread.wait_to_finish()

func _process(_delta: float) -> void:
	if not update_thread.is_alive():
		update_thread.start(update_nav_fields_thread_func)
		
func update_nav_fields_thread_func() -> void:
	# initialize thead local vars that depend on the main thread's vars
	update_mutex.lock()
	var rows = level_data.grid.size()
	var cols = level_data.grid[0].size()
	var obstruction_grid = level_data.grid.duplicate(true)
	update_mutex.unlock()

	# initialize thread local vars
	var local_stop_thread = false
	var distance_field = initialize_filled_field(rows, cols, INF)
	var vec_field_WORKER = initialize_filled_field(rows, cols, Vector2(0, 0))
	
	while not local_stop_thread:
		# get the target's position and the stop thread flag
		# from the main thread
		update_mutex.lock()
		local_stop_thread = stop_thread
		var target_pos:Vector2 = (target.global_position / level_data.TILE_SIZE).floor()
		update_mutex.unlock()

		# run the update functions
		var start_time = Time.get_ticks_msec()
		update_distance_field(distance_field, obstruction_grid, target_pos)
		print("Time taken for update_distance_field: ", Time.get_ticks_msec() - start_time, " ms")
		
		start_time = Time.get_ticks_msec()
		update_gradient_field(vec_field_WORKER, distance_field, rows, cols)
		print("Time taken for update_gradient_field: ", Time.get_ticks_msec() - start_time, " ms")

		# sync the worker thread's copy of the vector field back to the main thread
		update_mutex.lock()
		vec_field_MAIN = vec_field_WORKER.duplicate(true)  # Update the main thread's copy of the vector field
		update_mutex.unlock()


func update_distance_field(distance_field, obstruction_grid, target_pos:Vector2) -> void:
	# reset distance field
	var rows = distance_field.size()
	var cols = distance_field[0].size()
	for i in range(rows):
		for j in range(cols):
			distance_field[i][j] = INF
	distance_field[target_pos.y][target_pos.x] = 0.0 as float

	var directions = [
		Vector2(1, 0), Vector2(-1, 0), Vector2(0, 1), Vector2(0, -1),
		Vector2(1, 1), Vector2(-1, 1), Vector2(1, -1), Vector2(-1, -1)
	]

	var queue = [target_pos]

	while queue.size() > 0:
		var current = queue.pop_front()

		for direction in directions:
			var neighbor = current + direction
			var step_distance:float = current.distance_to(neighbor)

			if is_valid(neighbor,obstruction_grid):
				var new_distance = (distance_field[current.y][current.x] as float) + step_distance

				if new_distance < distance_field[neighbor.y][neighbor.x]:
					distance_field[neighbor.y][neighbor.x] = new_distance
					queue.append(neighbor)


func is_valid(pos: Vector2, obstruction_grid:Array) -> bool:
	var num_rows = obstruction_grid.size()
	var num_cols = obstruction_grid[0].size()
	return (pos.x >= 0 and pos.y >= 0 and pos.x < num_cols and pos.y < num_rows \
	and obstruction_grid[pos.y][pos.x] != 1)

func update_gradient_field(gradient_field, distance_field: Array, rows: int, cols: int) -> Array:
	for row in range(rows):
			for col in range(cols):
					var dx: float
					var dy: float
					if col > 0 and col < cols - 1:
							dx = (distance_field[row][col + 1] - distance_field[row][col - 1]) / 2.0
					elif col > 0:
							dx = distance_field[row][col] - distance_field[row][col - 1]
					else:
							dx = distance_field[row][col + 1] - distance_field[row][col]

					if row > 0 and row < rows - 1:
							dy = (distance_field[row + 1][col] - distance_field[row - 1][col]) / 2.0
					elif row > 0:
							dy = distance_field[row][col] - distance_field[row - 1][col]
					else:
							dy = distance_field[row + 1][col] - distance_field[row][col]

					if is_nan(dx) or is_inf(dx):
							dx = 0.0
					if is_nan(dy) or is_inf(dy):
							dy = 0.0

					var gradient = Vector2(dx, dy)
					if gradient.length() > 0:  # Add this check
							gradient = gradient.normalized()
					gradient_field[row][col] = gradient
	return gradient_field

From my experience with errors caused by multi-threading, the error messages tend to not hold much meaning and usually are due to race-conditions that are causing dirty writes and dirty reads. I suggest you look for a race condition due to poor synchronization code and removing it (this will likely solve your error)

godot_dev_ | 2023-04-24 19:28

After a lot of thrashing about and trial and error changes, it seems that the problem came down to the line

vec_field_MAIN = vec_field_WORKER.duplicate(true)  # Update the main thread's copy of the vector field

I’ve replaced that with a for loop and it seems to work fine now:

	for i in range(rows):
		for j in range(cols):
			vec_field_MAIN[i][j]	= vec_field_WORKER[i][j]

I have no idea why that fixed it, but hopefully this will help someone in the future :slight_smile:

petracula | 2023-04-24 20:04

the duplicate function might have some hidden compoenent that interacts with the scene tree? In any case your new solution just makes a copy of a references, so that should be thread safe

godot_dev_ | 2023-04-24 21:18