## Does the GPU batch actually run on the GPU, and does it agree with the CPU?
##
## Both halves matter and the second is the one that is easy to fake. The batch
## falls back to the CPU per node, silently, for four separate reasons -- so a
## bake-vs-bake comparison passes by comparing the CPU against itself and says
## nothing at all. ProcGeometryNode.get_last_exec_report() is what makes the
## difference visible: node type -> "gpu" | "cpu...".
##
## The graph is the shape the real ones have: a primitive, a per-vertex FIELD,
## and a geometry node consuming it.
##
## MeshGrid -> SetPosition(Offset <- VectorMath(Position * 0.25))
##
## Nothing here is noise, so CPU and GPU must agree to floating-point slack
## rather than to a tolerance anybody had to argue about.
extends SceneTree
const EPS := 1e-4
var _fail := 0
func _ck(n: String, ok: bool, d: String) -> void:
print(" %-46s %s %s" % [n, "PASS" if ok else "FAIL", d])
if not ok:
_fail += 1
func _build() -> ProcNodeGraph:
var g := ProcNodeGraph.new()
var grid := g.add_node(MeshGridNode.new())
g.get_node(grid).set_input_default(0, 4.0) # size x
g.get_node(grid).set_input_default(1, 4.0) # size y
g.get_node(grid).set_input_default(2, 16) # verts x
g.get_node(grid).set_input_default(3, 16) # verts y
var pos := g.add_node(PositionNode.new())
var vm := g.add_node(VectorMathNode.new())
g.get_node(vm).set_input_default(1, Vector3(0.25, 0.25, 0.25))
g.connect_sockets(pos, 0, vm, 0)
var sp := g.add_node(SetPositionNode.new())
g.connect_sockets(grid, 0, sp, 0)
g.connect_sockets(vm, 0, sp, 3) # Offset
return g
## ⛔ THE SHAPE THE FIRST GRAPH CANNOT TEST.
##
## _build()'s chain is fully fused, so every producer is inside the batch and
## there is nothing outside for a node to misread. That is the right assertion
## on the wrong shape, and it passed while the bridged mountain came out flat.
##
## A Switch has no supports_gpu() override, so _is_gpu_value_node rejects it and
## it can never join a batch -- which is exactly where the mountain's height sat.
## Its consumer, Combine XYZ, IS fusable, so the batch ends up reading a producer
## that is not in the shader. GlslEmitContext::in() falls back to the socket's
## LITERAL there, which compiles and computes something else: offset 0 instead
## of 5, no error anywhere.
##
## MeshGrid -> SetPosition(Offset <- CombineXYZ(Z <- Switch(true, 0.0, 5.0)))
func _build_outside_producer() -> ProcNodeGraph:
var g := ProcNodeGraph.new()
var grid := g.add_node(MeshGridNode.new())
g.get_node(grid).set_input_default(0, 4.0)
g.get_node(grid).set_input_default(1, 4.0)
g.get_node(grid).set_input_default(2, 8)
g.get_node(grid).set_input_default(3, 8)
var sw := g.add_node(SwitchNode.new())
g.get_node(sw).set("input_type", 0) # Float, as Blender's was
g.get_node(sw).set_input_default(0, true) # take the True branch
g.get_node(sw).set_input_default(1, 0.0) # False
g.get_node(sw).set_input_default(2, 5.0) # True -- the displacement
var comb := g.add_node(CombineXYZNode.new())
g.connect_sockets(sw, 0, comb, 2) # Z
var sp := g.add_node(SetPositionNode.new())
g.connect_sockets(grid, 0, sp, 0)
g.connect_sockets(comb, 0, sp, 3) # Offset
return g
func _bake_graph(g: ProcNodeGraph, use_gpu: bool) -> PackedVector3Array:
var host := ProcGeometryNode.new()
host.use_gpu = use_gpu
host.set_graph(g)
get_root().add_child(host)
var m: ArrayMesh = host.bake_mesh()
if m == null or m.get_surface_count() == 0:
return PackedVector3Array()
return m.surface_get_arrays(0)[Mesh.ARRAY_VERTEX]
func _bake(use_gpu: bool) -> PackedVector3Array:
var host := ProcGeometryNode.new()
host.use_gpu = use_gpu
host.set_graph(_build())
get_root().add_child(host)
var m: ArrayMesh = host.bake_mesh()
if m == null or m.get_surface_count() == 0:
return PackedVector3Array()
return m.surface_get_arrays(0)[Mesh.ARRAY_VERTEX]
func _init() -> void:
var cpu := _bake(false)
_ck("CPU bake produced a mesh", cpu.size() > 0, "%d verts" % cpu.size())
var gpu := _bake(true)
var report: Dictionary = ProcGeometryNode.get_last_exec_report()
_ck("GPU bake produced a mesh", gpu.size() > 0, "%d verts" % gpu.size())
print(" execution report: %s" % report)
# ⛔ THE ASSERTION THAT STOPS THIS BEING A NO-OP. Without it the two bakes
# agree because they were the same bake.
_ck("SetPosition ran on the GPU", report.get("SetPosition", "absent") == "gpu",
"reported '%s'" % report.get("SetPosition", "absent"))
# VectorMath is deliberately NOT asserted: Position is a lazy field producer,
# so its consumer is deferred and materialised inside SetPosition rather than
# executed in its own right. What matters is that the node consuming the
# field did not bail out for want of a channel to read it through.
var bailed := []
for k in report.keys():
if String(report[k]).begins_with("cpu (a connected input"):
bailed.append(k)
_ck("no node bailed for an unreadable input", bailed.is_empty(),
"bailed: %s" % [bailed])
if cpu.size() == gpu.size() and cpu.size() > 0:
var worst := 0.0
for i in range(cpu.size()):
worst = max(worst, (cpu[i] - gpu[i]).length())
_ck("CPU and GPU agree", worst < EPS, "worst |dp| = " + String.num(worst, 9))
else:
_ck("CPU and GPU agree", false,
"vertex counts differ: %d vs %d" % [cpu.size(), gpu.size()])
# ── A PRODUCER OUTSIDE THE BATCH ────────────────────────────────────────
var og := _build_outside_producer()
var ocpu := _bake_graph(og, false)
var ogpu := _bake_graph(og, true)
var orep: Dictionary = ProcGeometryNode.get_last_exec_report()
print(" outside-producer report: %s" % orep)
var cpu_lift := 0.0
for p in ocpu:
cpu_lift = max(cpu_lift, absf(p.z))
_ck("the outside-producer graph displaces at all", cpu_lift > 4.9,
"CPU max |z| = " + String.num(cpu_lift, 4) + ", Switch True is 5.0")
if ocpu.size() == ogpu.size() and ocpu.size() > 0:
var oworst := 0.0
for i in range(ocpu.size()):
oworst = max(oworst, (ocpu[i] - ogpu[i]).length())
# ⛔ THE ASSERTION THAT WAS MISSING. Without the batch refusal the GPU
# reads Combine XYZ's Z as its literal 0.0 and this is 5.0.
_ck("CPU and GPU agree when a producer is outside the batch",
oworst < EPS, "worst |dp| = " + String.num(oworst, 9))
else:
_ck("CPU and GPU agree when a producer is outside the batch", false,
"vertex counts differ: %d vs %d" % [ocpu.size(), ogpu.size()])
print("\n%d/%d passed" % [8 - _fail, 8])
quit(1 if _fail > 0 else 0)