Repository: godot
Issue: Changes that should be inconsequential result in crashes and inconsistencies when a thread is involved.
Labels: bug,topic:core,needs testing,crash

### Godot version

v4.0.2.stable.official [7a0977ce2]

### System information

Apple M1 Max, MacBook Pro 16-inch, 2021, 64 GB, macOS Ventura 13.3 (22E252)

### Issue description

I have encountered a situation where simply reading (not changing) some constants from multiple threads appears to completely and totally change program behavior to the point where it will randomly crash or corrupt the on-screen results.

I was working on a Minecraft clone and I started migrating my chunk generation to threads when I started having all manner of difficulties with crashes and corrupted meshes. At first I assumed I was doing something wrong, but as I tried to isolate the problem, I kept deleting code and removing elements and found myself becoming more and more confused and now I'm am reasonably confident that I must have found a bug. During this process I uncovered three separate and seemingly innocuous code changes that could each trigger trouble for me and have documented them below and demonstrated them in the attached video.

Since this involves multithreading, I would not be surprised if it turns out to be difficult to reproduce it, unfortunately. I have only tested this on my mac, so there's a chance it doesn't even happen on Windows/Linux, but it occurs repeatedly and reliably for me as you will see.

I am using the standard release of Godot 4.0.2 as downloaded from https://godotengine.org. I do not know if this is new in this version as I only just encountered it about 3 days ago and I have spent all of my free time since pulling everything I could out of my project to narrow down and isolate a reasonably small reproducible (at least for me) case.

### Steps to reproduce

The exact steps to reproduce (at least for me) is to download and open my sample project and simply try to run it a few times. You should see that it crashes or doesn't always display properly. Sometimes it just hangs. Other times it causes macOS to popup the standard "app crashed" dialog box.

I put together a video to demonstrate all of this, but Github says it's too big so I split it up into parts. In case that goes wrong or you want to see the full resolution video, get it here: https://www.dropbox.com/s/ts42enjh5su8b0q/godot_threading_bug.mov?dl=0

Here's what the program should look like when it's running correctly:

https://user-images.githubusercontent.com/453721/230800010-6435a1ba-d6c2-464a-bc37-9fc2c2ab9e9f.mov


Now, here's some short snippets going through the different ways to trigger the problems.

There are at least three very simple bits of code that trigger these behaviors as I tried to show in the videos.

The first is in `Global.gd`. This script is installed as an Autoload named `Global`. As demonstrated in the video below, the `BLOCKS` array is initially this:

`var BLOCKS := [ .. etc ..]`

However, simply changing the `var` to `const` will cause the program to crash:

https://user-images.githubusercontent.com/453721/230800057-c85295f6-d03f-4ce7-aaf4-6685014c4739.mov


The crash is usually shown as one of the lines in the `update_geometry` function, but that's not always the case. As you can see in the video, the crash even moves around - the line that's blamed changes each time I run it and sometimes it manages to launch, but you can see the visuals are incorrect.

When I change `const` back to `var`, the program once against seems to work fine:

https://user-images.githubusercontent.com/453721/230800072-68e1f442-ba97-4865-b7c5-de74b4272107.mov


The next change is, IMO, even stranger - simply commenting out the enum constant `SOLID` will also change the results! `SOLID` isn't even being used in the program at all and yet it's presence changes things!

https://user-images.githubusercontent.com/453721/230800089-28ca6c37-aeb5-47f4-9cd7-a0f6dfff8b8f.mov


What's totally wild is renaming `SOLID` to some other name will *also* work! It just doesn't like the name `SOLID`, I guess? I can even leave the `BLOCKS` array declared as `const` and the program will still work as long as I change the name:

https://user-images.githubusercontent.com/453721/230800111-3f1db90e-e0d8-4dc8-a96e-5ef48ed300a5.mov


As you can see, changing things back to the way they were before will once again trigger the crashing:

https://user-images.githubusercontent.com/453721/230800141-94566aa6-1a5d-4887-a140-9ef39b5e4fcf.mov


And the final trigger I found is to simply comment out an *unused* line of code in the `set_block_id()` function. If I remove that line, then I can leave `SOLID` in place and leave the `BLOCKS` array as `const` and the program somehow still works:

https://user-images.githubusercontent.com/453721/230800185-153fa259-8428-414a-ad79-fd64fae864a6.mov


And once again, simply putting the line back - even though it isn't used for anything - results in crashing again:

https://user-images.githubusercontent.com/453721/230800216-71d6b1a3-e407-4f36-b6a3-c431ea976a99.mov


I'm reasonably baffled by all of this.

When the program first starts, it attempts to create 5 "chunks" of terrain using a single worker thread which is hardcoded to only make these five chunks - nothing especially fancy is going on here and I've removed tons of stuff to get a simple reproduction. Here's all of `Main.gd`:

```gdscript
extends Node3D

const ChunkScene := preload("res://Chunk.tscn")

var thread := Thread.new()

func _ready() -> void:
	thread.start(thread_function)

func thread_function() -> void:
	for chunk_position in [Vector3i(0, 0, 0), Vector3i(0, 0, -1), Vector3i(0, 0, 1), Vector3i(1, 0, 0), Vector3i(-1, 0, 0)]:
		var chunk := make_chunk(chunk_position)
		back_on_main.call_deferred(chunk_position, chunk)

func back_on_main(chunk_position: Vector3i, chunk: Chunk) -> void:
	chunk.position = chunk_position * Global.CHUNK_SIZE
	add_child(chunk)

func make_chunk(chunk_position: Vector3i) -> Chunk:
	var chunk_origin: Vector3i = chunk_position * Global.CHUNK_SIZE
	var chunk: Chunk = ChunkScene.instantiate()

	var noise := FastNoiseLite.new()
	noise.frequency = 0.0025
	noise.fractal_octaves = 10
	noise.seed = 0

	for z in Global.CHUNK_SIZE.z:
		for x in Global.CHUNK_SIZE.x:
			var global_x := chunk_origin.x + x
			var global_z := chunk_origin.z + z
			var height := ceili(((noise.get_noise_2d(global_x, global_z) + 1) / 2) * 192)
				
			for y in range(0, height):
				var block_id := 1

				if y >= height - 5:
					block_id = 2

				chunk.set_block_id(Vector3i(x, y, z), block_id)
				
	return chunk
```

Each chunk is an instance of a chunk scene and the blocks are all set before it is added to the scene tree. The adding happens with a `call_deferred` call so it runs on the main thread as Godot expects and after that, the chunk instances aren't touched by any other threads as far as I'm aware.

The `make_chunk()` function isn't very complicated - it just uses noise to make a terrain. In my testing, this doesn't seem to matter very much - adding a bunch of hardcoded blocks also seems to break things, so I don't think using noise has anything to do with anything.

The code in `Chunk.gd` is a bit more complicated in places and is pretty dumb in this example, but that's because I removed as much as I could from my bigger project to build this reproduction. It is likely that most of this isn't necessary to trigger the bug(s), but it does help make it super easy to see when the bug occurs so I left it here.

I hesitate to even guess what's causing this, but at a high level it almost has to be some kind of memory corruption - simply changing the name of `SOLID` to `SOLID1` really shouldn't have changed anything, and yet it clearly does. My initial assumption was that `const` vs. `var` might be the primary culprit, but then other changes seem to negate that. The very confusing case of simply commenting out that one unused line of code in `set_block_id()` suggests that maybe accessing a dictionary has some kind of subtle role to play in all this?

In any case... I really wish someone luck in figuring this one out and I really hope someone else will be able to reproduce it.

### Minimal reproduction project

[GodotThreadingBug.zip](https://github.com/godotengine/godot/files/11186439/GodotThreadingBug.zip)