OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes commits / d2f27e8

gate: the emitted DENSITY, against the CPU, on a real device
RED as of this commit, and for the right reason: 0/100 points, and the emitted density identically zero on the bridged voronoi mountain. test_emitted_op_parity asks whether one node's arithmetic survives emission, at one position. It cannot see which axis the density is read from, or whether the graph's frame was honoured, because those are properties of the whole function and of no node in it. Every node can pass while the GRAPH emits nothing. The invariant is flat and is not a tolerance on character: proc_density(p) from the shader EQUALS evaluate_density_batch([p]) from the CPU, at every p. Two paths that disagree are two different worlds, and the terrain gets whichever one runs -- which depends on whether a RenderingDevice came up. The lattice is asymmetric on purpose. A frame error is a permutation of the axes, so samples along a diagonal, where x == z, agree under a swap and prove nothing. One call site, positions from an SSBO, not a literal per point: the first version baked proc_density(vec3(...)) per sample the way the op gate does, and glslc inlines every call before it optimises. A density carrying a Voronoi cell search inside an octave loop, inlined a hundred times, went past this gate's own 4 GB cap and died with LLVM ERROR: out of memory. density_probe reports WHICH AXIS MOVED rather than the density alone. A graph that writes its height into an axis the density does not read answers zero, and that is indistinguishable from a flat field unless the displacement is shown. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
d2f27e82c6f453db28dc33cbcfef24de6b9d17e4
by plankatron <fleeky@gmail.com> · 2026-09-08T13:01:35.000Z

Changed files (3) +507 -0

demo/density_probe.gd +39 -0
……@@ -0,0 +1,39 @@
1## Evaluate a bridged graph at sample points and report WHICH AXIS MOVED.
2##
3## evaluate_density_batch returns dst.y - src.y, so a graph that writes its
4## height into another component answers zero. Reporting the whole displacement
5## is the only way to tell "the field is flat here" from "the field went
6## somewhere the density does not read".
7##
8## Args: <graph.json>
9extends SceneTree
10func _init() -> void:
11 var a := OS.get_cmdline_user_args()
12 var g: ProcNodeGraph = GraphImporter.import_from_json(FileAccess.get_file_as_string(a[0]))
13 if g == null:
14 push_error("import returned null"); quit(1); return
15 var pts := PackedVector3Array()
16 for i in range(9):
17 var t := -16.0 + 4.0 * float(i)
18 pts.append(Vector3(t, 0.0, t))
19 var arr := []
20 arr.resize(Mesh.ARRAY_MAX)
21 arr[Mesh.ARRAY_VERTEX] = pts
22 var am := ArrayMesh.new()
23 am.add_surface_from_arrays(Mesh.PRIMITIVE_POINTS, arr)
24 var host := ProcGeometryNode.new()
25 host.use_gpu = false
26 host.set_input_mesh(am)
27 host.set_graph(g)
28 get_root().add_child(host)
29 var m: ArrayMesh = host.bake_mesh()
30 var d: PackedFloat32Array = g.evaluate_density_batch(pts)
31 if m == null or m.get_surface_count() == 0:
32 print("no mesh out"); quit(1); return
33 var v: PackedVector3Array = m.surface_get_arrays(0)[Mesh.ARRAY_VERTEX]
34 print(" in -> out displacement density")
35 for i in range(mini(v.size(), pts.size())):
36 var dd := v[i] - pts[i]
37 print(" (%6.1f,%5.1f,%6.1f) -> (%7.2f,%7.2f,%7.2f) d(%6.2f,%6.2f,%6.2f) %9.4f"
38 % [pts[i].x, pts[i].y, pts[i].z, v[i].x, v[i].y, v[i].z, dd.x, dd.y, dd.z, d[i]])
39 quit()
demo/glsl_density_gate_dump.gd +65 -0
……@@ -0,0 +1,65 @@
1## Stage 1 of the emitted-DENSITY gate: the CPU's density beside the GLSL the
2## engine emits for the same graph.
3##
4## WHY THIS EXISTS SEPARATELY FROM glsl_op_gate_dump.
5##
6## The op gate asks "does this node's arithmetic survive emission" and answers it
7## at a single position. That cannot see anything about the graph AS A WHOLE --
8## which axis the density is read from, whether the emitter honours the frame a
9## bridged graph was authored in, whether the value cone reaches the density at
10## all. Those are properties of the whole function, and they are where a graph
11## that emits 30 correct nodes can still emit a field that is identically zero.
12##
13## THE INVARIANT: proc_density(p) from the shader must equal
14## evaluate_density_batch([p]) from the CPU, for every p. Not "close in
15## character" -- the same number. Two paths that disagree are two different
16## worlds, and the terrain gets whichever one is running.
17##
18## Usage: godot --headless --path demo --script res://glsl_density_gate_dump.gd \
19## -- <graph.json> <out-dir>
20extends SceneTree
21
22func _init() -> void:
23 var a := OS.get_cmdline_user_args()
24 if a.size() < 2:
25 push_error("need <graph.json> <out-dir>"); quit(1); return
26 var g: ProcNodeGraph = GraphImporter.import_from_json(FileAccess.get_file_as_string(a[0]))
27 if g == null:
28 push_error("import returned null"); quit(1); return
29
30 var glsl: String = g.compile_to_glsl_function("proc_density", false, PackedStringArray())
31 if glsl.is_empty():
32 # A refusal is a legitimate answer and not this gate's business -- it
33 # already printed which node. Say so and leave, rather than writing an
34 # empty case stage 2 would report as a compile failure.
35 print("DENSITY_GATE_DUMP: EMISSION REFUSED, nothing to compare")
36 quit(2); return
37
38 # A LATTICE, not a line. A frame error is a permutation of the axes, and a
39 # sample set that is symmetric in two of them cannot see one: points along
40 # (t, 0, t) have x == z, so a shader that swapped them would agree
41 # everywhere. The steps are deliberately unequal per axis for the same
42 # reason.
43 var pts := PackedVector3Array()
44 for ix in range(5):
45 for iy in range(4):
46 for iz in range(5):
47 pts.append(Vector3(-18.0 + 9.0 * ix, -1.0 + 7.0 * iy, -16.0 + 8.5 * iz))
48
49 var cpu: PackedFloat32Array = g.evaluate_density_batch(pts)
50 if cpu.size() != pts.size():
51 push_error("density batch returned %d for %d points" % [cpu.size(), pts.size()])
52 quit(1); return
53
54 var dir: String = a[1]
55 DirAccess.make_dir_recursive_absolute(dir)
56 FileAccess.open(dir + "/density.glsl", FileAccess.WRITE).store_string(glsl)
57 var manifest := ""
58 for i in range(pts.size()):
59 manifest += "%d %s %s %s %s\n" % [i,
60 String.num(pts[i].x, 9), String.num(pts[i].y, 9),
61 String.num(pts[i].z, 9), String.num(cpu[i], 9)]
62 FileAccess.open(dir + "/manifest.txt", FileAccess.WRITE).store_string(manifest)
63 print("DENSITY_GATE_DUMP: %d points, blender_frame=%s -> %s"
64 % [pts.size(), str(g.get_source_frame() == 1), dir])
65 quit()
tests/test_emitted_density_parity.cpp +403 -0
……@@ -0,0 +1,403 @@
1// ============================================================================
2// EMITTED-DENSITY PARITY — is the shader's field the same field as the CPU's?
3//
4// WHAT THIS SEES THAT NOTHING ELSE DOES.
5//
6// test_emitted_op_parity asks whether a node's arithmetic survives emission,
7// one node at a time, at one position. Every one of those can pass while the
8// GRAPH emits a density that is wrong or absent, because "which axis is the
9// density read from" and "was this graph authored in Blender's frame" are
10// properties of the whole function and of no node in it.
11//
12// Measured 2026-09-08 on the bridged voronoi mountain: thirty nodes of Voronoi,
13// cone and noise emitted correctly, computed the height into `pos.z` because
14// the graph was authored in Blender, and the emitted tail read `pos.y -
15// query_y`. The density was identically zero. `evaluate_density_batch` returned
16// a real mountain for the same graph, because GraphExecutor converts the frame
17// at the end (graph_executor.cpp, _to_godot_frame) and the emitter has never
18// heard of the flag that drives it -- grep is_blender_frame src/gpu/ is empty.
19// So the CPU and the GPU were two different worlds and nothing compared them.
20//
21// THE INVARIANT, and it is deliberately not a tolerance on character:
22// proc_density(p) from the shader EQUALS evaluate_density_batch([p]) from the
23// CPU, at every p. If those two disagree the terrain gets whichever one runs,
24// and which one runs depends on whether a RenderingDevice came up.
25//
26// The lattice comes from stage 1 and is asymmetric on purpose. A frame error is
27// a permutation of the axes, so samples along a diagonal -- where x == z --
28// agree under a swap and prove nothing.
29//
30// Build (no engine, no godot-cpp, no scons):
31// g++ -O2 -std=c++17 -o /tmp/density_parity tests/test_emitted_density_parity.cpp -lvulkan
32//
33// Run:
34// godot --headless --path demo --script res://glsl_density_gate_dump.gd -- graph.json /tmp/dgate
35// /tmp/density_parity /tmp/dgate
36//
37// Exit status is 0 when every point agrees.
38// ============================================================================
39
40#include <vulkan/vulkan.h>
41#include <sys/resource.h>
42
43#include <cmath>
44#include <cstdio>
45#include <cstdlib>
46#include <cstring>
47#include <fstream>
48#include <sstream>
49#include <string>
50#include <vector>
51
52// A float32 op on two machines will not be bit-identical: the CPU uses libm and
53// the GPU its own approximations, and sin/pow/log are where they part company.
54// 1e-5 relative is far tighter than any difference that would change a surface
55// and far looser than the last bit.
56static const double TOL_REL = 1e-5;
57static const double TOL_ABS = 1e-6;
58
59// ⚠ THIS GATE EMITS ONE FUNCTION PER CASE, so its shader grows with the case
60// list. That is fine for cheap arithmetic and NOT fine for anything with an
61// octave loop in it: the sibling gate, built the same way, twice took a 31 GB
62// machine to global OOM once perlin cases were added to it
63// (tests/test_shared_noise_parity.cpp, which is now data-driven and does not).
64// Keep the expensive families over there and only enough cases here to show
65// the EMITTER wires a node correctly. The cap makes a mistake fail alone.
66static const rlim_t MEM_LIMIT_BYTES = (rlim_t)4 * 1024 * 1024 * 1024;
67
68static void cap_own_memory() {
69 struct rlimit rl;
70 if (getrlimit(RLIMIT_AS, &rl) != 0) { return; }
71 if (rl.rlim_cur != RLIM_INFINITY && rl.rlim_cur <= MEM_LIMIT_BYTES) { return; }
72 rl.rlim_cur = (rl.rlim_max == RLIM_INFINITY || rl.rlim_max > MEM_LIMIT_BYTES)
73 ? MEM_LIMIT_BYTES : rl.rlim_max;
74 if (setrlimit(RLIMIT_AS, &rl) != 0) {
75 fprintf(stderr, "WARNING: setrlimit failed; running with NO memory cap\n");
76 }
77}
78
79static void die(const char *what, VkResult r) {
80 fprintf(stderr, "FATAL: %s -> VkResult %d\n", what, (int)r);
81 struct rlimit rl;
82 if (getrlimit(RLIMIT_AS, &rl) == 0 && rl.rlim_cur != RLIM_INFINITY) {
83 fprintf(stderr, " Capped at %.1f GiB of address space, deliberately.\n"
84 " If it is being hit, the case list has outgrown a gate that\n"
85 " emits one function per case.\n",
86 (double)rl.rlim_cur / (1024.0 * 1024.0 * 1024.0));
87 }
88 exit(1);
89}
90#define VK(x) do { VkResult _r = (x); if (_r != VK_SUCCESS) die(#x, _r); } while (0)
91
92struct Sample {
93 int idx = 0;
94 float x = 0, y = 0, z = 0;
95 double cpu = 0;
96};
97
98static std::string read_file(const std::string &path) {
99 std::ifstream f(path);
100 if (!f) {
101 fprintf(stderr, "FATAL: cannot read %s\n", path.c_str());
102 exit(1);
103 }
104 std::stringstream ss;
105 ss << f.rdbuf();
106 return ss.str();
107}
108
109static std::vector<uint32_t> compile_glsl(const std::string &src) {
110 char in_path[] = "/tmp/proc_op_parity_XXXXXX";
111 int fd = mkstemp(in_path);
112 if (fd < 0) { perror("mkstemp"); exit(1); }
113 FILE *f = fdopen(fd, "w");
114 fwrite(src.data(), 1, src.size(), f);
115 fclose(f);
116 std::string spv = std::string(in_path) + ".spv";
117 std::string cmd = "glslc -fshader-stage=comp -o " + spv + " " + in_path;
118 if (system(cmd.c_str()) != 0) {
119 fprintf(stderr, "FATAL: glslc failed (source left at %s)\n", in_path);
120 exit(1);
121 }
122 FILE *sf = fopen(spv.c_str(), "rb");
123 fseek(sf, 0, SEEK_END); long n = ftell(sf); fseek(sf, 0, SEEK_SET);
124 std::vector<uint32_t> out(n / 4);
125 if (fread(out.data(), 1, n, sf) != (size_t)n) { fprintf(stderr, "FATAL: short read\n"); exit(1); }
126 fclose(sf);
127 remove(in_path); remove(spv.c_str());
128 return out;
129}
130
131struct Ctx {
132 VkInstance instance = VK_NULL_HANDLE;
133 VkPhysicalDevice phys = VK_NULL_HANDLE;
134 VkDevice dev = VK_NULL_HANDLE;
135 VkQueue queue = VK_NULL_HANDLE;
136 uint32_t qfam = 0;
137 VkCommandPool cmdpool = VK_NULL_HANDLE;
138 VkPhysicalDeviceMemoryProperties memprops{};
139 char devname[256] = {0};
140};
141
142static uint32_t find_mem(Ctx &c, uint32_t bits, VkMemoryPropertyFlags want) {
143 for (uint32_t i = 0; i < c.memprops.memoryTypeCount; i++) {
144 if ((bits & (1u << i)) && (c.memprops.memoryTypes[i].propertyFlags & want) == want) {
145 return i;
146 }
147 }
148 fprintf(stderr, "FATAL: no memory type with flags 0x%x\n", want);
149 exit(1);
150}
151
152static void init_vulkan(Ctx &c) {
153 VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO};
154 app.pApplicationName = "proc-nodes emitted-op parity";
155 app.apiVersion = VK_API_VERSION_1_1;
156 VkInstanceCreateInfo ii{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO};
157 ii.pApplicationInfo = &app;
158 VK(vkCreateInstance(&ii, nullptr, &c.instance));
159
160 uint32_t n = 0;
161 VK(vkEnumeratePhysicalDevices(c.instance, &n, nullptr));
162 if (!n) { fprintf(stderr, "FATAL: no Vulkan device\n"); exit(1); }
163 std::vector<VkPhysicalDevice> devs(n);
164 VK(vkEnumeratePhysicalDevices(c.instance, &n, devs.data()));
165 c.phys = devs[0];
166 for (auto d : devs) {
167 VkPhysicalDeviceProperties p;
168 vkGetPhysicalDeviceProperties(d, &p);
169 if (p.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) { c.phys = d; break; }
170 }
171 VkPhysicalDeviceProperties props;
172 vkGetPhysicalDeviceProperties(c.phys, &props);
173 snprintf(c.devname, sizeof(c.devname), "%s", props.deviceName);
174 vkGetPhysicalDeviceMemoryProperties(c.phys, &c.memprops);
175
176 uint32_t qn = 0;
177 vkGetPhysicalDeviceQueueFamilyProperties(c.phys, &qn, nullptr);
178 std::vector<VkQueueFamilyProperties> qs(qn);
179 vkGetPhysicalDeviceQueueFamilyProperties(c.phys, &qn, qs.data());
180 bool found = false;
181 for (uint32_t i = 0; i < qn; i++) {
182 if (qs[i].queueFlags & VK_QUEUE_COMPUTE_BIT) { c.qfam = i; found = true; break; }
183 }
184 if (!found) { fprintf(stderr, "FATAL: no compute queue\n"); exit(1); }
185
186 float prio = 1.0f;
187 VkDeviceQueueCreateInfo qi{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO};
188 qi.queueFamilyIndex = c.qfam; qi.queueCount = 1; qi.pQueuePriorities = &prio;
189 VkDeviceCreateInfo di{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO};
190 di.queueCreateInfoCount = 1; di.pQueueCreateInfos = &qi;
191 VK(vkCreateDevice(c.phys, &di, nullptr, &c.dev));
192 vkGetDeviceQueue(c.dev, c.qfam, 0, &c.queue);
193
194 VkCommandPoolCreateInfo ci{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
195 ci.queueFamilyIndex = c.qfam;
196 ci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
197 VK(vkCreateCommandPool(c.dev, &ci, nullptr, &c.cmdpool));
198}
199
200// Replace every occurrence of `from` with `to`.
201static void replace_all(std::string &s, const std::string &from, const std::string &to) {
202 size_t p = 0;
203 while ((p = s.find(from, p)) != std::string::npos) {
204 s.replace(p, from.size(), to);
205 p += to.size();
206 }
207}
208
209int main(int argc, char **argv) {
210 cap_own_memory();
211 const std::string dir = (argc > 1) ? std::string(argv[1]) : std::string("/tmp/proc_density_gate");
212
213 std::vector<Sample> pts;
214 {
215 std::ifstream mf(dir + "/manifest.txt");
216 if (!mf) { fprintf(stderr, "FATAL: no manifest.txt in %s\n", dir.c_str()); return 1; }
217 std::string line;
218 while (std::getline(mf, line)) {
219 if (line.empty()) continue;
220 std::istringstream is(line);
221 Sample s;
222 is >> s.idx >> s.x >> s.y >> s.z >> s.cpu;
223 pts.push_back(s);
224 }
225 }
226 if (pts.empty()) { fprintf(stderr, "FATAL: no sample points\n"); return 1; }
227
228 const std::string glsl = read_file(dir + "/density.glsl");
229 const std::string fn = "float proc_density";
230 const size_t p = glsl.find(fn);
231 if (p == std::string::npos) { fprintf(stderr, "FATAL: no proc_density in density.glsl\n"); return 1; }
232
233 // ONE CALL SITE, and the positions arrive in a buffer.
234 //
235 // The first version baked each sample as a literal `proc_density(vec3(...))`
236 // -- the shape the op gate uses, where each case is a different function.
237 // Here it is ONE function called N times, and glslc inlines every call
238 // before it optimises: a density carrying a Voronoi cell search inside an
239 // octave loop, inlined a hundred times, took the compiler past this gate's
240 // own 4 GB cap and it died with LLVM ERROR: out of memory. A gate that
241 // takes the machine down is a gate nobody runs, which is why that cap is
242 // there at all.
243 const std::string prelude = glsl.substr(0, p);
244 const std::string body = glsl.substr(p);
245 std::string src =
246 "#version 450\n"
247 "layout(local_size_x = 64) in;\n"
248 "layout(std430, binding = 0) buffer Out { float v[]; } out_v;\n"
249 "layout(std430, binding = 1) readonly buffer In { vec4 p[]; } in_p;\n" +
250 prelude + body +
251 "void main() {\n"
252 " uint i = gl_GlobalInvocationID.x;\n"
253 " if (i >= " + std::to_string(pts.size()) + "u) return;\n"
254 " out_v.v[i] = proc_density(in_p.p[i].xyz);\n"
255 "}\n";
256
257 Ctx c{};
258 init_vulkan(c);
259 printf("device: %s\npoints: %zu\n\n", c.devname, pts.size());
260
261 std::vector<uint32_t> spv = compile_glsl(src);
262
263 VkBuffer buf, inbuf;
264 VkDeviceMemory mem, inmem;
265 void *mapped = nullptr;
266 void *inmapped = nullptr;
267 const VkDeviceSize bytes = sizeof(float) * pts.size();
268 const VkDeviceSize inbytes = sizeof(float) * 4 * pts.size();
269 {
270 VkBufferCreateInfo bi{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
271 bi.size = bytes;
272 bi.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
273 VK(vkCreateBuffer(c.dev, &bi, nullptr, &buf));
274 VkMemoryRequirements mr;
275 vkGetBufferMemoryRequirements(c.dev, buf, &mr);
276 VkMemoryAllocateInfo ai{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
277 ai.allocationSize = mr.size;
278 ai.memoryTypeIndex = find_mem(c, mr.memoryTypeBits,
279 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
280 VK(vkAllocateMemory(c.dev, &ai, nullptr, &mem));
281 VK(vkBindBufferMemory(c.dev, buf, mem, 0));
282 VK(vkMapMemory(c.dev, mem, 0, VK_WHOLE_SIZE, 0, &mapped));
283 memset(mapped, 0, bytes);
284 }
285 {
286 // vec4 per point, not vec3: std430 pads a vec3 to 16 bytes anyway, and
287 // writing three floats into a stride of four is the kind of mismatch
288 // that reads as a field error rather than as a packing one.
289 VkBufferCreateInfo bi{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
290 bi.size = inbytes;
291 bi.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
292 VK(vkCreateBuffer(c.dev, &bi, nullptr, &inbuf));
293 VkMemoryRequirements mr;
294 vkGetBufferMemoryRequirements(c.dev, inbuf, &mr);
295 VkMemoryAllocateInfo ai{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
296 ai.allocationSize = mr.size;
297 ai.memoryTypeIndex = find_mem(c, mr.memoryTypeBits,
298 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
299 VK(vkAllocateMemory(c.dev, &ai, nullptr, &inmem));
300 VK(vkBindBufferMemory(c.dev, inbuf, inmem, 0));
301 VK(vkMapMemory(c.dev, inmem, 0, VK_WHOLE_SIZE, 0, &inmapped));
302 float *fp = (float *)inmapped;
303 for (size_t i = 0; i < pts.size(); i++) {
304 fp[i * 4 + 0] = pts[i].x;
305 fp[i * 4 + 1] = pts[i].y;
306 fp[i * 4 + 2] = pts[i].z;
307 fp[i * 4 + 3] = 0.0f;
308 }
309 }
310
311 VkShaderModuleCreateInfo smi{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
312 smi.codeSize = spv.size() * sizeof(uint32_t);
313 smi.pCode = spv.data();
314 VkShaderModule mod;
315 VK(vkCreateShaderModule(c.dev, &smi, nullptr, &mod));
316
317 VkDescriptorSetLayoutBinding b[2]{};
318 for (int i = 0; i < 2; i++) {
319 b[i].binding = (uint32_t)i; b[i].descriptorCount = 1;
320 b[i].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
321 b[i].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
322 }
323 VkDescriptorSetLayoutCreateInfo dli{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
324 dli.bindingCount = 2; dli.pBindings = b;
325 VkDescriptorSetLayout dlayout;
326 VK(vkCreateDescriptorSetLayout(c.dev, &dli, nullptr, &dlayout));
327 VkPipelineLayoutCreateInfo pli{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
328 pli.setLayoutCount = 1; pli.pSetLayouts = &dlayout;
329 VkPipelineLayout playout;
330 VK(vkCreatePipelineLayout(c.dev, &pli, nullptr, &playout));
331
332 VkPipelineShaderStageCreateInfo st{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO};
333 st.stage = VK_SHADER_STAGE_COMPUTE_BIT; st.module = mod; st.pName = "main";
334 VkComputePipelineCreateInfo cpi{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO};
335 cpi.stage = st; cpi.layout = playout;
336 VkPipeline pipe;
337 VK(vkCreateComputePipelines(c.dev, VK_NULL_HANDLE, 1, &cpi, nullptr, &pipe));
338
339 VkDescriptorPoolSize ps{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2};
340 VkDescriptorPoolCreateInfo dpi{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
341 dpi.maxSets = 1; dpi.poolSizeCount = 1; dpi.pPoolSizes = &ps;
342 VkDescriptorPool dpool;
343 VK(vkCreateDescriptorPool(c.dev, &dpi, nullptr, &dpool));
344 VkDescriptorSetAllocateInfo dai{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
345 dai.descriptorPool = dpool; dai.descriptorSetCount = 1; dai.pSetLayouts = &dlayout;
346 VkDescriptorSet dset;
347 VK(vkAllocateDescriptorSets(c.dev, &dai, &dset));
348 VkDescriptorBufferInfo dbi[2] = {{buf, 0, VK_WHOLE_SIZE}, {inbuf, 0, VK_WHOLE_SIZE}};
349 VkWriteDescriptorSet w[2]{};
350 for (int i = 0; i < 2; i++) {
351 w[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
352 w[i].dstSet = dset; w[i].dstBinding = (uint32_t)i; w[i].descriptorCount = 1;
353 w[i].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; w[i].pBufferInfo = &dbi[i];
354 }
355 vkUpdateDescriptorSets(c.dev, 2, w, 0, nullptr);
356
357 VkCommandBufferAllocateInfo cbi{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
358 cbi.commandPool = c.cmdpool; cbi.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbi.commandBufferCount = 1;
359 VkCommandBuffer cmd;
360 VK(vkAllocateCommandBuffers(c.dev, &cbi, &cmd));
361 VkCommandBufferBeginInfo bbi{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
362 bbi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
363 VK(vkBeginCommandBuffer(cmd, &bbi));
364 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, pipe);
365 vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, playout, 0, 1, &dset, 0, nullptr);
366 vkCmdDispatch(cmd, (uint32_t)((pts.size() + 63) / 64), 1, 1);
367 VK(vkEndCommandBuffer(cmd));
368 VkSubmitInfo si{VK_STRUCTURE_TYPE_SUBMIT_INFO};
369 si.commandBufferCount = 1; si.pCommandBuffers = &cmd;
370 VK(vkQueueSubmit(c.queue, 1, &si, VK_NULL_HANDLE));
371 VK(vkQueueWaitIdle(c.queue));
372
373 const float *got = (const float *)mapped;
374 int bad = 0;
375 int gpu_all_zero = 1;
376 int cpu_all_zero = 1;
377 for (auto &s : pts) {
378 const double g = got[s.idx];
379 if (g != 0.0) gpu_all_zero = 0;
380 if (s.cpu != 0.0) cpu_all_zero = 0;
381 const double diff = fabs(g - s.cpu);
382 const double scale = fabs(s.cpu) > 1.0 ? fabs(s.cpu) : 1.0;
383 const bool nan_both = std::isnan(g) && std::isnan(s.cpu);
384 const bool ok = nan_both || diff <= TOL_ABS || (diff / scale) <= TOL_REL;
385 if (!ok) {
386 bad++;
387 if (bad <= 12) {
388 printf(" FAIL (%8.3f,%8.3f,%8.3f) cpu=%-14g gpu=%-14g d=%g\n",
389 s.x, s.y, s.z, s.cpu, g, diff);
390 }
391 }
392 }
393 if (bad > 12) printf(" ... %d more\n", bad - 12);
394 // A FIELD THAT IS FLAT EVERYWHERE IS THE SPECIFIC FAILURE THIS GATE WAS
395 // BUILT FOR, and it deserves to be said rather than left to be read out of
396 // a column of zeroes. Both directions: an all-zero CPU means the graph
397 // computes nothing, an all-zero GPU with a live CPU means the emission
398 // dropped the field on the floor.
399 if (gpu_all_zero) printf("\n ** THE EMITTED DENSITY IS ZERO AT EVERY POINT **\n");
400 if (cpu_all_zero) printf("\n ** THE CPU DENSITY IS ZERO AT EVERY POINT **\n");
401 printf("\n%d/%zu points agree\n", (int)pts.size() - bad, pts.size());
402 return bad ? 1 : 0;
403}