OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_ocean_fft.gd

95 lines · gd ref:
extends SceneTree
## Does the vendored FFT ocean pipeline actually produce displacement, IN THIS
## PROJECT, on the main RenderingDevice?
##
## Everything else in the ocean work -- clipmap, shore attenuation, shoaling,
## foam -- reads the displacement and normal maps. If they come back empty,
## none of it can be judged, and an empty map looks exactly like a flat sea,
## which is also what a correct sea looks like from far enough away. So check
## the maps themselves rather than a screenshot.
##
## dc-terrain holds its own device via create_local_rendering_device(); this
## uses RenderingServer.get_rendering_device(). hyper-games #58 is about
## starving a SECOND VkDevice during bring-up, and this adds no third one --
## but running them together is exactly what has not been tried, so the gate
## reports whether the terrain device is alive alongside.
##
##   DISPLAY=:0 "$HYPER_TPS_GODOT" --path . --rendering-driver vulkan \
##       --script res://scenes/test/gate_ocean_fft.gd
const MAP_SIZE := 256
var _gen
# MUST be a typed array: WaveGenerator.update takes
# Array[WaveCascadeParameters] and GDScript rejects an untyped one at the
# call, which surfaces as 'generator has no descriptors' two steps later.
var _params: Array[WaveCascadeParameters] = []
var _frames := 0

func _initialize() -> void:
	_gen = load("res://assets/ocean/wave_generator.gd").new()
	_gen.map_size = MAP_SIZE
	root.add_child(_gen)
	var p := WaveCascadeParameters.new()
	p.tile_length = Vector2(50, 50)
	p.wind_speed = 20.0
	p.displacement_scale = 1.0
	p.normal_scale = 1.0
	p.time = 120.0
	_params.append(p)
	var q := WaveCascadeParameters.new()
	q.tile_length = Vector2(200, 200)
	q.wind_speed = 20.0
	q.time = 120.0 + PI
	_params.append(q)
	print("GATE: FFT ocean, map %d^2, %d cascades, main device" % [MAP_SIZE, _params.size()])

func _process(delta: float) -> bool:
	_frames += 1
	# ⚠ WaveGenerator DISPATCHES IN ITS OWN _process, not in update(); update()
	# only queues cascades. And a GDScript SceneTree subclass that defines
	# _process REPLACES SceneTree's C++ main-loop implementation, so child nodes
	# never process at all -- this gate was preventing the exact work it was
	# testing, and the symptom was a correctly-read, entirely zero map. So drive
	# the generator's frame step by hand.
	if _frames == 1:
		_gen.update(delta, _params)      # first call only inits the GPU + queues
		return false
	if _frames <= 2 + _params.size():
		_gen.update(delta, _params)
		_gen._process(delta)             # one cascade per step, as the node would
		return false

	var rd := RenderingServer.get_rendering_device()
	if rd == null:
		print("FAIL — no main RenderingDevice (headless?)"); quit(1); return true
	var disp = _gen.descriptors.get(&'displacement_map')
	var norm = _gen.descriptors.get(&'normal_map')
	if disp == null or norm == null:
		print("FAIL — generator has no displacement/normal descriptors"); quit(1); return true

	var ok := true
	for pair in [["displacement", disp], ["normal", norm]]:
		var name: String = pair[0]
		var bytes: PackedByteArray = rd.texture_get_data(pair[1].rid, 0)
		if bytes.is_empty():
			print("  %s map: FAIL — readback empty" % name); ok = false; continue
		# RGBA16F: decode as half floats via PackedByteArray.
		var n := bytes.size() / 2
		var mn := 1e30; var mx := -1e30; var nonzero := 0
		for i in range(0, n, 4):            # stride: sample one channel in 4
			var h := bytes.decode_half(i * 2)
			if is_nan(h) or is_inf(h): continue
			mn = min(mn, h); mx = max(mx, h)
			if absf(h) > 1e-6: nonzero += 1
		print("  %-12s map: %d bytes, range %+.4f .. %+.4f, %d non-zero samples"
			% [name, bytes.size(), mn, mx, nonzero])
		if nonzero == 0 or mx - mn < 1e-5:
			print("             FAIL — map is flat; the FFT produced nothing")
			ok = false
	if ok:
		print("PASS — the FFT populates both maps on the main device.")
		quit(0)
	else:
		print("FAIL — see above.")
		quit(1)
	return true