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