Gododin Gododin Documentation

Linked repository source

fixtures/native_components/reload.gd

This read-only view comes from the file in the Gododin repository. It retains its design, example or evidence context.

Download the original file · Return to the guide

@tool
extends EditorPlugin

var nodes: Array[Node] = []
var resources: Array[Resource] = []
var identities := PackedInt64Array()
var retained_strings: Array[String] = []
var last_revision := 0
var signals_seen := 0
var last_signal := 0
var layout := 0
var root: Node2D
var started := false
# Phase 15 baseline consumers: a baked-asset-shaped grid, a per-revision bulk
# input, staged/classify expectations, and the background verification state.
var baked_grid := PackedByteArray()
var baked_digest := 0
const BAKED_WIDTH := 64
const BAKED_HEIGHT := 48
const CELL := 16.0
var bg_pending_id := 0
var bg_pending_ticks := 0


func _enter_tree() -> void:
	call_deferred("start")


func require(condition: bool, message: String) -> void:
	if not condition:
		push_error("GODODIN_RELOAD_LIFETIME_FAIL " + message)
		get_tree().quit(1)
		assert(condition, message)


# The GDScript oracle for the native mask summary: FNV-1a folded exactly as
# the authored body folds it, truncated the same way. GDScript integers are
# signed 64-bit, so the basis is written in its signed form and the multiply
# wraps modulo 2^64 like Odin's u64 arithmetic.
func mask_digest(bytes: PackedByteArray) -> int:
	var accumulator := -3750763034362895579 # 0xcbf29ce484222325
	for byte in bytes:
		accumulator = (accumulator ^ byte) * 0x100000001b3
	return accumulator & 0x3fffffffffffffff


func start() -> void:
	await get_tree().create_timer(3.0).timeout
	root = Node2D.new()
	root.name = "UnsavedReloadScene"
	EditorInterface.add_root_node(root)
	var count := int(OS.get_environment("GODODIN_RELOAD_TEST_COUNT"))
	for index in count:
		var node: Node = ClassDB.instantiate("ReloadNode")
		var settings: Resource = ClassDB.instantiate("ReloadSettings")
		require(node != null and settings != null, "native classes unavailable")
		root.add_child(node)
		node.owner = root
		node.set("seed", 400 + index)
		node.set("label", "node-%d" % index)
		settings.set("seed", 800 + index)
		settings.set("label", "resource-%d" % index)
		node.connect("pulse", on_pulse)
		nodes.append(node)
		resources.append(settings)
		identities.append(node.get_instance_id())
		identities.append(settings.get_instance_id())
	# The baked-asset consumer: one immutable grid whose digest the native bulk
	# input must reproduce every revision, the way an exported app consumes a
	# committed baked asset.
	baked_grid.resize(BAKED_WIDTH * BAKED_HEIGHT)
	for y in BAKED_HEIGHT:
		for x in BAKED_WIDTH:
			baked_grid[y * BAKED_WIDTH + x] = 255 if ((x * 13 + y * 29) % 97) < 40 else 0
	baked_digest = mask_digest(baked_grid)
	EditorInterface.mark_scene_as_unsaved()
	started = true
	set_process(true)


func on_pulse(revision: int) -> void:
	signals_seen += 1
	last_signal = revision


func _process(_delta: float) -> void:
	if not started or nodes.is_empty():
		return
	if FileAccess.file_exists(OS.get_environment("GODODIN_RELOAD_TEST_STOP")):
		set_process(false)
		# Free through Godot while the current native image is still active.
		for node in nodes:
			node.free()
		nodes.clear()
		resources.clear()
		get_tree().quit()
		return
	# A submitted background computation completes on a later tick; finishing
	# it here leaves one unconsumed record in the handoff, which the next
	# reload's teardown must drain.
	if bg_pending_id != 0:
		bg_pending_ticks += 1
		var status := int(nodes[0].call("bg_status"))
		if status == 2:
			var fingerprint: int = nodes[0].call("bg_poll")
			require(fingerprint != 0, "a completed background record polled empty")
			# Leave a fresh unconsumed submission outstanding across the next
			# reload; its teardown cancels, joins, and drains it.
			var outstanding: int = nodes[0].call("bg_submit", 0x5eed + last_revision, 250_000_000)
			require(outstanding > 0, "the outstanding background submit was rejected")
			bg_pending_id = 0
		elif status == 3 or status == 4 or bg_pending_ticks > 3000:
			require(false, "the background computation never completed (status %d)" % status)
		return
	if not nodes[0].has_method("revision"):
		require(false, "native class was not reconstructed")
		set_process(false)
		return
	var revision := int(nodes[0].call("revision"))
	if revision == last_revision:
		return
	require(revision == last_revision + 1, "a superseded build reached native activation")
	for index in nodes.size():
		var node := nodes[index]
		var settings := resources[index]
		require(node.get_instance_id() == identities[index*2] and settings.get_instance_id() == identities[index*2+1], "Godot identity changed")
		require(node.get("seed") == 400 + index and settings.get("seed") == 800 + index, "stored values changed")
		require(node.get("label") == "node-%d" % index and settings.get("label") == "resource-%d" % index, "owned string changed")
		require(node.call("private_value") == 1000 + revision, "private state was not reconstructed")
		require(node.call("cache") == "cache-%d" % revision, "allocator-backed state was not reconstructed")
		require(node.call("literal") == "literal-%d" % revision, "literal came from old image")
		var previous_signals := signals_seen
		require(node.call("ping") and signals_seen == previous_signals + 1 and last_signal == revision, "native signal connection is stale")
		if layout == 0:
			layout = node.call("layout")
		require(node.call("layout") == layout, "layout fixture changed size/alignment")
	for index in retained_strings.size():
		require(retained_strings[index] == "literal-%d" % (index + 1), "a surviving Godot String retained image storage")
	retained_strings.append(nodes[0].call("literal"))
	require(root.scene_file_path.is_empty() and EditorInterface.get_edited_scene_root() == root, "unsaved scene changed")

	# --- Phase 15 consumers, verified against GDScript oracles every revision ---
	var probe := nodes[0]
	# The reloaded image's own worker code answers, never a stale entry.
	require(int(probe.call("bg_version")) == revision, "the background worker came from an old image")
	# A previous reload with an outstanding submission drained it: the fresh
	# instance starts idle with an empty handoff.
	require(int(probe.call("bg_status")) == 0 and int(probe.call("bg_poll")) == 0,
		"pre-reload background residue survived the reload")
	# Baked-asset consumer: the immutable grid digests identically.
	require(int(probe.call("mask_summary", baked_grid, BAKED_WIDTH, BAKED_HEIGHT)) == baked_digest,
		"the baked grid digest diverged")
	# Bulk input, revision-derived.
	var bulk := PackedByteArray()
	bulk.resize(32 * 24)
	for index in bulk.size():
		bulk[index] = (index * 5 + revision * 3) & 0xff
	require(int(probe.call("mask_summary", bulk, 32, 24)) == mask_digest(bulk),
		"the borrowed packed input diverged")
	# Staged bulk result, copied by the Host from the instance's own buffer.
	var staged: PackedByteArray = probe.call("stage_flags", 512)
	require(staged.size() == 512, "the staged result carried the wrong count")
	for index in staged.size():
		require(staged[index] == ((index * 7 + 11) & 0xff) ^ (revision & 0xff),
			"the staged result diverged at %d" % index)
	# Typed-vector consumer over borrowed packed arrays.
	var positions := PackedVector3Array()
	var radii := PackedFloat32Array()
	for index in 16:
		positions.append(Vector3(float((index * 17) % 128), float(index * 3), float((index * 31) % 96)))
		radii.append(4.0 + index)
	var flags: PackedByteArray = probe.call("classify", positions, radii, Vector3.ZERO, CELL)
	require(flags.size() == 16, "the classify result carried the wrong count")
	for index in 16:
		var pair := int(positions[index].x / CELL) + int(positions[index].z / CELL)
		require(flags[index] == (255 if pair % 2 == 0 else 0), "the classify result diverged at %d" % index)

	last_revision = revision
	var observation := {"revision": revision, "editor_pid": OS.get_process_id(), "nodes": nodes.size(), "resources": resources.size(), "layout": layout, "signals_seen": signals_seen, "host": OS.get_environment("GODODIN_NATIVE_ACTIVE_HOST")}
	var file := FileAccess.open(OS.get_environment("GODODIN_RELOAD_TEST_OBSERVATION"), FileAccess.WRITE)
	file.store_string(JSON.stringify(observation))
	file.close()
	print("GODODIN_RELOAD_LIFETIME_OBSERVED " + JSON.stringify(observation))
	# Kick the background verification for this revision; completion (and the
	# next reload's drain) is handled on later ticks at the top of _process.
	var submitted: int = probe.call("bg_submit", 0x51b5 + revision, 30_000_000)
	require(submitted > 0, "the background submit was rejected")
	bg_pending_id = submitted
	bg_pending_ticks = 0