// ============================================================================
// EMITTED-DENSITY PARITY — is the shader's field the same field as the CPU's?
//
// WHAT THIS SEES THAT NOTHING ELSE DOES.
//
// test_emitted_op_parity asks whether a node's arithmetic survives emission,
// one node at a time, at one position. Every one of those can pass while the
// GRAPH emits a density that is wrong or absent, because "which axis is the
// density read from" and "was this graph authored in Blender's frame" are
// properties of the whole function and of no node in it.
//
// Measured 2026-09-08 on the bridged voronoi mountain: thirty nodes of Voronoi,
// cone and noise emitted correctly, computed the height into `pos.z` because
// the graph was authored in Blender, and the emitted tail read `pos.y -
// query_y`. The density was identically zero. `evaluate_density_batch` returned
// a real mountain for the same graph, because GraphExecutor converts the frame
// at the end (graph_executor.cpp, _to_godot_frame) and the emitter has never
// heard of the flag that drives it -- grep is_blender_frame src/gpu/ is empty.
// So the CPU and the GPU were two different worlds and nothing compared them.
//
// THE INVARIANT, and it is deliberately not a tolerance on character:
// proc_density(p) from the shader EQUALS evaluate_density_batch([p]) from the
// CPU, at every p. If those two disagree the terrain gets whichever one runs,
// and which one runs depends on whether a RenderingDevice came up.
//
// The lattice comes from stage 1 and 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.
//
// Build (no engine, no godot-cpp, no scons):
// g++ -O2 -std=c++17 -o /tmp/density_parity tests/test_emitted_density_parity.cpp -lvulkan
//
// Run:
// godot --headless --path demo --script res://glsl_density_gate_dump.gd -- graph.json /tmp/dgate
// /tmp/density_parity /tmp/dgate
//
// Exit status is 0 when every point agrees.
// ============================================================================
#include <vulkan/vulkan.h>
#include <sys/resource.h>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
// A float32 op on two machines will not be bit-identical: the CPU uses libm and
// the GPU its own approximations, and sin/pow/log are where they part company.
// 1e-5 relative is far tighter than any difference that would change a surface
// and far looser than the last bit.
static const double TOL_REL = 1e-5;
static const double TOL_ABS = 1e-6;
// ⚠ THIS GATE EMITS ONE FUNCTION PER CASE, so its shader grows with the case
// list. That is fine for cheap arithmetic and NOT fine for anything with an
// octave loop in it: the sibling gate, built the same way, twice took a 31 GB
// machine to global OOM once perlin cases were added to it
// (tests/test_shared_noise_parity.cpp, which is now data-driven and does not).
// Keep the expensive families over there and only enough cases here to show
// the EMITTER wires a node correctly. The cap makes a mistake fail alone.
static const rlim_t MEM_LIMIT_BYTES = (rlim_t)4 * 1024 * 1024 * 1024;
static void cap_own_memory() {
struct rlimit rl;
if (getrlimit(RLIMIT_AS, &rl) != 0) { return; }
if (rl.rlim_cur != RLIM_INFINITY && rl.rlim_cur <= MEM_LIMIT_BYTES) { return; }
rl.rlim_cur = (rl.rlim_max == RLIM_INFINITY || rl.rlim_max > MEM_LIMIT_BYTES)
? MEM_LIMIT_BYTES : rl.rlim_max;
if (setrlimit(RLIMIT_AS, &rl) != 0) {
fprintf(stderr, "WARNING: setrlimit failed; running with NO memory cap\n");
}
}
static void die(const char *what, VkResult r) {
fprintf(stderr, "FATAL: %s -> VkResult %d\n", what, (int)r);
struct rlimit rl;
if (getrlimit(RLIMIT_AS, &rl) == 0 && rl.rlim_cur != RLIM_INFINITY) {
fprintf(stderr, " Capped at %.1f GiB of address space, deliberately.\n"
" If it is being hit, the case list has outgrown a gate that\n"
" emits one function per case.\n",
(double)rl.rlim_cur / (1024.0 * 1024.0 * 1024.0));
}
exit(1);
}
#define VK(x) do { VkResult _r = (x); if (_r != VK_SUCCESS) die(#x, _r); } while (0)
struct Sample {
int idx = 0;
float x = 0, y = 0, z = 0;
double cpu = 0;
};
static std::string read_file(const std::string &path) {
std::ifstream f(path);
if (!f) {
fprintf(stderr, "FATAL: cannot read %s\n", path.c_str());
exit(1);
}
std::stringstream ss;
ss << f.rdbuf();
return ss.str();
}
static std::vector<uint32_t> compile_glsl(const std::string &src) {
char in_path[] = "/tmp/proc_op_parity_XXXXXX";
int fd = mkstemp(in_path);
if (fd < 0) { perror("mkstemp"); exit(1); }
FILE *f = fdopen(fd, "w");
fwrite(src.data(), 1, src.size(), f);
fclose(f);
std::string spv = std::string(in_path) + ".spv";
std::string cmd = "glslc -fshader-stage=comp -o " + spv + " " + in_path;
if (system(cmd.c_str()) != 0) {
fprintf(stderr, "FATAL: glslc failed (source left at %s)\n", in_path);
exit(1);
}
FILE *sf = fopen(spv.c_str(), "rb");
fseek(sf, 0, SEEK_END); long n = ftell(sf); fseek(sf, 0, SEEK_SET);
std::vector<uint32_t> out(n / 4);
if (fread(out.data(), 1, n, sf) != (size_t)n) { fprintf(stderr, "FATAL: short read\n"); exit(1); }
fclose(sf);
remove(in_path); remove(spv.c_str());
return out;
}
struct Ctx {
VkInstance instance = VK_NULL_HANDLE;
VkPhysicalDevice phys = VK_NULL_HANDLE;
VkDevice dev = VK_NULL_HANDLE;
VkQueue queue = VK_NULL_HANDLE;
uint32_t qfam = 0;
VkCommandPool cmdpool = VK_NULL_HANDLE;
VkPhysicalDeviceMemoryProperties memprops{};
char devname[256] = {0};
};
static uint32_t find_mem(Ctx &c, uint32_t bits, VkMemoryPropertyFlags want) {
for (uint32_t i = 0; i < c.memprops.memoryTypeCount; i++) {
if ((bits & (1u << i)) && (c.memprops.memoryTypes[i].propertyFlags & want) == want) {
return i;
}
}
fprintf(stderr, "FATAL: no memory type with flags 0x%x\n", want);
exit(1);
}
static void init_vulkan(Ctx &c) {
VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO};
app.pApplicationName = "proc-nodes emitted-op parity";
app.apiVersion = VK_API_VERSION_1_1;
VkInstanceCreateInfo ii{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO};
ii.pApplicationInfo = &app;
VK(vkCreateInstance(&ii, nullptr, &c.instance));
uint32_t n = 0;
VK(vkEnumeratePhysicalDevices(c.instance, &n, nullptr));
if (!n) { fprintf(stderr, "FATAL: no Vulkan device\n"); exit(1); }
std::vector<VkPhysicalDevice> devs(n);
VK(vkEnumeratePhysicalDevices(c.instance, &n, devs.data()));
c.phys = devs[0];
for (auto d : devs) {
VkPhysicalDeviceProperties p;
vkGetPhysicalDeviceProperties(d, &p);
if (p.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) { c.phys = d; break; }
}
VkPhysicalDeviceProperties props;
vkGetPhysicalDeviceProperties(c.phys, &props);
snprintf(c.devname, sizeof(c.devname), "%s", props.deviceName);
vkGetPhysicalDeviceMemoryProperties(c.phys, &c.memprops);
uint32_t qn = 0;
vkGetPhysicalDeviceQueueFamilyProperties(c.phys, &qn, nullptr);
std::vector<VkQueueFamilyProperties> qs(qn);
vkGetPhysicalDeviceQueueFamilyProperties(c.phys, &qn, qs.data());
bool found = false;
for (uint32_t i = 0; i < qn; i++) {
if (qs[i].queueFlags & VK_QUEUE_COMPUTE_BIT) { c.qfam = i; found = true; break; }
}
if (!found) { fprintf(stderr, "FATAL: no compute queue\n"); exit(1); }
float prio = 1.0f;
VkDeviceQueueCreateInfo qi{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO};
qi.queueFamilyIndex = c.qfam; qi.queueCount = 1; qi.pQueuePriorities = &prio;
VkDeviceCreateInfo di{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO};
di.queueCreateInfoCount = 1; di.pQueueCreateInfos = &qi;
VK(vkCreateDevice(c.phys, &di, nullptr, &c.dev));
vkGetDeviceQueue(c.dev, c.qfam, 0, &c.queue);
VkCommandPoolCreateInfo ci{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
ci.queueFamilyIndex = c.qfam;
ci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
VK(vkCreateCommandPool(c.dev, &ci, nullptr, &c.cmdpool));
}
// Replace every occurrence of `from` with `to`.
static void replace_all(std::string &s, const std::string &from, const std::string &to) {
size_t p = 0;
while ((p = s.find(from, p)) != std::string::npos) {
s.replace(p, from.size(), to);
p += to.size();
}
}
int main(int argc, char **argv) {
cap_own_memory();
const std::string dir = (argc > 1) ? std::string(argv[1]) : std::string("/tmp/proc_density_gate");
std::vector<Sample> pts;
{
std::ifstream mf(dir + "/manifest.txt");
if (!mf) { fprintf(stderr, "FATAL: no manifest.txt in %s\n", dir.c_str()); return 1; }
std::string line;
while (std::getline(mf, line)) {
if (line.empty()) continue;
std::istringstream is(line);
Sample s;
is >> s.idx >> s.x >> s.y >> s.z >> s.cpu;
pts.push_back(s);
}
}
if (pts.empty()) { fprintf(stderr, "FATAL: no sample points\n"); return 1; }
const std::string glsl = read_file(dir + "/density.glsl");
const std::string fn = "float proc_density";
const size_t p = glsl.find(fn);
if (p == std::string::npos) { fprintf(stderr, "FATAL: no proc_density in density.glsl\n"); return 1; }
// ONE CALL SITE, and the positions arrive in a buffer.
//
// The first version baked each sample as a literal `proc_density(vec3(...))`
// -- the shape the op gate uses, where each case is a different function.
// Here it is ONE function called N times, and glslc inlines every call
// before it optimises: a density carrying a Voronoi cell search inside an
// octave loop, inlined a hundred times, took the compiler past this gate's
// own 4 GB cap and it died with LLVM ERROR: out of memory. A gate that
// takes the machine down is a gate nobody runs, which is why that cap is
// there at all.
const std::string prelude = glsl.substr(0, p);
const std::string body = glsl.substr(p);
std::string src =
"#version 450\n"
"layout(local_size_x = 64) in;\n"
"layout(std430, binding = 0) buffer Out { float v[]; } out_v;\n"
"layout(std430, binding = 1) readonly buffer In { vec4 p[]; } in_p;\n" +
prelude + body +
"void main() {\n"
" uint i = gl_GlobalInvocationID.x;\n"
" if (i >= " + std::to_string(pts.size()) + "u) return;\n"
" out_v.v[i] = proc_density(in_p.p[i].xyz);\n"
"}\n";
Ctx c{};
init_vulkan(c);
printf("device: %s\npoints: %zu\n\n", c.devname, pts.size());
std::vector<uint32_t> spv = compile_glsl(src);
VkBuffer buf, inbuf;
VkDeviceMemory mem, inmem;
void *mapped = nullptr;
void *inmapped = nullptr;
const VkDeviceSize bytes = sizeof(float) * pts.size();
const VkDeviceSize inbytes = sizeof(float) * 4 * pts.size();
{
VkBufferCreateInfo bi{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
bi.size = bytes;
bi.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
VK(vkCreateBuffer(c.dev, &bi, nullptr, &buf));
VkMemoryRequirements mr;
vkGetBufferMemoryRequirements(c.dev, buf, &mr);
VkMemoryAllocateInfo ai{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
ai.allocationSize = mr.size;
ai.memoryTypeIndex = find_mem(c, mr.memoryTypeBits,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
VK(vkAllocateMemory(c.dev, &ai, nullptr, &mem));
VK(vkBindBufferMemory(c.dev, buf, mem, 0));
VK(vkMapMemory(c.dev, mem, 0, VK_WHOLE_SIZE, 0, &mapped));
memset(mapped, 0, bytes);
}
{
// vec4 per point, not vec3: std430 pads a vec3 to 16 bytes anyway, and
// writing three floats into a stride of four is the kind of mismatch
// that reads as a field error rather than as a packing one.
VkBufferCreateInfo bi{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
bi.size = inbytes;
bi.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
VK(vkCreateBuffer(c.dev, &bi, nullptr, &inbuf));
VkMemoryRequirements mr;
vkGetBufferMemoryRequirements(c.dev, inbuf, &mr);
VkMemoryAllocateInfo ai{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
ai.allocationSize = mr.size;
ai.memoryTypeIndex = find_mem(c, mr.memoryTypeBits,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
VK(vkAllocateMemory(c.dev, &ai, nullptr, &inmem));
VK(vkBindBufferMemory(c.dev, inbuf, inmem, 0));
VK(vkMapMemory(c.dev, inmem, 0, VK_WHOLE_SIZE, 0, &inmapped));
float *fp = (float *)inmapped;
for (size_t i = 0; i < pts.size(); i++) {
fp[i * 4 + 0] = pts[i].x;
fp[i * 4 + 1] = pts[i].y;
fp[i * 4 + 2] = pts[i].z;
fp[i * 4 + 3] = 0.0f;
}
}
VkShaderModuleCreateInfo smi{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
smi.codeSize = spv.size() * sizeof(uint32_t);
smi.pCode = spv.data();
VkShaderModule mod;
VK(vkCreateShaderModule(c.dev, &smi, nullptr, &mod));
VkDescriptorSetLayoutBinding b[2]{};
for (int i = 0; i < 2; i++) {
b[i].binding = (uint32_t)i; b[i].descriptorCount = 1;
b[i].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
b[i].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
}
VkDescriptorSetLayoutCreateInfo dli{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
dli.bindingCount = 2; dli.pBindings = b;
VkDescriptorSetLayout dlayout;
VK(vkCreateDescriptorSetLayout(c.dev, &dli, nullptr, &dlayout));
VkPipelineLayoutCreateInfo pli{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
pli.setLayoutCount = 1; pli.pSetLayouts = &dlayout;
VkPipelineLayout playout;
VK(vkCreatePipelineLayout(c.dev, &pli, nullptr, &playout));
VkPipelineShaderStageCreateInfo st{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO};
st.stage = VK_SHADER_STAGE_COMPUTE_BIT; st.module = mod; st.pName = "main";
VkComputePipelineCreateInfo cpi{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO};
cpi.stage = st; cpi.layout = playout;
VkPipeline pipe;
VK(vkCreateComputePipelines(c.dev, VK_NULL_HANDLE, 1, &cpi, nullptr, &pipe));
VkDescriptorPoolSize ps{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2};
VkDescriptorPoolCreateInfo dpi{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
dpi.maxSets = 1; dpi.poolSizeCount = 1; dpi.pPoolSizes = &ps;
VkDescriptorPool dpool;
VK(vkCreateDescriptorPool(c.dev, &dpi, nullptr, &dpool));
VkDescriptorSetAllocateInfo dai{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
dai.descriptorPool = dpool; dai.descriptorSetCount = 1; dai.pSetLayouts = &dlayout;
VkDescriptorSet dset;
VK(vkAllocateDescriptorSets(c.dev, &dai, &dset));
VkDescriptorBufferInfo dbi[2] = {{buf, 0, VK_WHOLE_SIZE}, {inbuf, 0, VK_WHOLE_SIZE}};
VkWriteDescriptorSet w[2]{};
for (int i = 0; i < 2; i++) {
w[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
w[i].dstSet = dset; w[i].dstBinding = (uint32_t)i; w[i].descriptorCount = 1;
w[i].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; w[i].pBufferInfo = &dbi[i];
}
vkUpdateDescriptorSets(c.dev, 2, w, 0, nullptr);
VkCommandBufferAllocateInfo cbi{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
cbi.commandPool = c.cmdpool; cbi.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbi.commandBufferCount = 1;
VkCommandBuffer cmd;
VK(vkAllocateCommandBuffers(c.dev, &cbi, &cmd));
VkCommandBufferBeginInfo bbi{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
bbi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
VK(vkBeginCommandBuffer(cmd, &bbi));
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, pipe);
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, playout, 0, 1, &dset, 0, nullptr);
vkCmdDispatch(cmd, (uint32_t)((pts.size() + 63) / 64), 1, 1);
VK(vkEndCommandBuffer(cmd));
VkSubmitInfo si{VK_STRUCTURE_TYPE_SUBMIT_INFO};
si.commandBufferCount = 1; si.pCommandBuffers = &cmd;
VK(vkQueueSubmit(c.queue, 1, &si, VK_NULL_HANDLE));
VK(vkQueueWaitIdle(c.queue));
const float *got = (const float *)mapped;
int bad = 0;
int gpu_all_zero = 1;
int cpu_all_zero = 1;
for (auto &s : pts) {
const double g = got[s.idx];
if (g != 0.0) gpu_all_zero = 0;
if (s.cpu != 0.0) cpu_all_zero = 0;
const double diff = fabs(g - s.cpu);
const double scale = fabs(s.cpu) > 1.0 ? fabs(s.cpu) : 1.0;
const bool nan_both = std::isnan(g) && std::isnan(s.cpu);
const bool ok = nan_both || diff <= TOL_ABS || (diff / scale) <= TOL_REL;
if (!ok) {
bad++;
if (bad <= 12) {
printf(" FAIL (%8.3f,%8.3f,%8.3f) cpu=%-14g gpu=%-14g d=%g\n",
s.x, s.y, s.z, s.cpu, g, diff);
}
}
}
if (bad > 12) printf(" ... %d more\n", bad - 12);
// A FIELD THAT IS FLAT EVERYWHERE IS THE SPECIFIC FAILURE THIS GATE WAS
// BUILT FOR, and it deserves to be said rather than left to be read out of
// a column of zeroes. Both directions: an all-zero CPU means the graph
// computes nothing, an all-zero GPU with a live CPU means the emission
// dropped the field on the floor.
if (gpu_all_zero) printf("\n ** THE EMITTED DENSITY IS ZERO AT EVERY POINT **\n");
if (cpu_all_zero) printf("\n ** THE CPU DENSITY IS ZERO AT EVERY POINT **\n");
printf("\n%d/%zu points agree\n", (int)pts.size() - bad, pts.size());
return bad ? 1 : 0;
}