| … | … | @@ -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. |
| 56 | static const double TOL_REL = 1e-5; |
| 57 | static 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. |
| 66 | static const rlim_t MEM_LIMIT_BYTES = (rlim_t)4 * 1024 * 1024 * 1024; |
| 67 | |
| 68 | static 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 | |
| 79 | static 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 | |
| 92 | struct Sample { |
| 93 | int idx = 0; |
| 94 | float x = 0, y = 0, z = 0; |
| 95 | double cpu = 0; |
| 96 | }; |
| 97 | |
| 98 | static 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 | |
| 109 | static 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 | |
| 131 | struct 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 | |
| 142 | static 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 | |
| 152 | static 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`. |
| 201 | static 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 | |
| 209 | int 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 | } |