// ============================================================================
// EMITTED-GLSL OPERATION PARITY — does the shader compute what the graph does?
//
// WHY IT HAS TO BE THIS SHAPE.
//
// proc-nodes has two implementations of every node's arithmetic: the C++ that
// runs on the CPU, and the GLSL that compile_to_glsl_function emits for the
// terrain's density shader. Nothing compared them. `gate_dc_vs_lookdev` walks
// the CPU path and is blind to an emitted-GLSL change; the in-game oracle
// SUBMITS density fills rather than observing, which is what broke the terrain
// on 2026-09-03. So the only instrument that can see this is a standalone
// program with its own VkDevice, and this is it.
//
// NEITHER SIDE IS WRITTEN FOR THE TEST. The CPU answers come from
// ProcNodeGraph::evaluate_socket -- i.e. bmath::math_op, the function the
// Blender parity suites gate at 71/71 over all 41 operations. The GLSL is the
// ENGINE'S OWN EMISSION, dumped by demo/glsl_op_gate_dump.gd, not a
// transcription made here. That matters: the erosion filter's lesson was that a
// transcription used AS the reference can only agree with itself. A second
// expression CHECKED against the shipping one, over every branch, is a
// different thing.
//
// HOW. Stage 1 (headless Godot) writes a directory: manifest.txt with one line
// per case -- index, node, operation, a, b, c, the CPU's answer -- and
// case_NNNN.glsl holding the whole emitted function. This program renames each
// case's proc_density to case_NNNN, concatenates them under ONE copy of the
// prelude, compiles the result with glslc, and dispatches a single shader that
// writes every case's value into an SSBO. One compile and one dispatch for all
// of them, because 246 pipelines to answer 246 questions is a slow gate nobody
// runs.
//
// Build (no engine, no godot-cpp, no scons):
// g++ -O2 -std=c++17 -o /tmp/op_parity tests/test_emitted_op_parity.cpp -lvulkan
//
// Run:
// godot --headless --path demo --script res://glsl_op_gate_dump.gd -- /tmp/opgate
// /tmp/op_parity /tmp/opgate
//
// Exit status is 0 when every case 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 Case {
int idx = 0;
std::string node;
int op = 0;
float a = 0, b = 0, c = 0;
double cpu = 0;
std::string glsl;
};
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) ? argv[1] : "/tmp/proc_op_gate";
std::vector<Case> cases;
{
std::ifstream mf(dir + "/manifest.txt");
if (!mf) { fprintf(stderr, "FATAL: no manifest at %s\n", dir.c_str()); return 1; }
std::string line;
while (std::getline(mf, line)) {
if (line.empty()) continue;
Case k;
std::istringstream ls(line);
ls >> k.idx >> k.node >> k.op >> k.a >> k.b >> k.c >> k.cpu;
char buf[64];
snprintf(buf, sizeof(buf), "/case_%04d.glsl", k.idx);
k.glsl = read_file(dir + buf);
cases.push_back(k);
}
}
if (cases.empty()) { fprintf(stderr, "FATAL: no cases\n"); return 1; }
// ONE shader: the prelude once, then every case's function renamed.
const std::string fn = "float proc_density";
// EVERY case's prelude, not case 0's. Taking the first one alone was a bug
// this gate caught in itself: case 0 is a Math case, so the Vector Math
// cases 246 onward referenced helpers that had been discarded and 288
// compile errors came back. The preludes carry #ifndef guards, so
// concatenating all of them is safe even where one contains another.
std::string prelude;
for (auto &k : cases) {
size_t p = k.glsl.find(fn);
if (p == std::string::npos) { fprintf(stderr, "FATAL: no proc_density in case %d\n", k.idx); return 1; }
std::string pre = k.glsl.substr(0, p);
if (!pre.empty() && prelude.find(pre) == std::string::npos) {
prelude += pre;
}
}
std::string body;
for (auto &k : cases) {
size_t p = k.glsl.find(fn);
if (p == std::string::npos) { fprintf(stderr, "FATAL: no proc_density in case %d\n", k.idx); return 1; }
std::string f = k.glsl.substr(p);
char name[64];
snprintf(name, sizeof(name), "case_%04d", k.idx);
replace_all(f, "proc_density", name);
body += f + "\n";
}
std::string calls;
for (auto &k : cases) {
char c[128];
snprintf(c, sizeof(c), " out_v.v[%d] = case_%04d(vec3(0.0));\n", k.idx, k.idx);
calls += c;
}
std::string src =
"#version 450\n"
"layout(local_size_x = 1) in;\n"
"layout(std430, binding = 0) buffer Out { float v[]; } out_v;\n" +
prelude + body +
"void main() {\n" + calls + "}\n";
Ctx c{};
init_vulkan(c);
printf("device: %s\ncases : %zu\n\n", c.devname, cases.size());
std::vector<uint32_t> spv = compile_glsl(src);
VkBuffer buf;
VkDeviceMemory mem;
void *mapped = nullptr;
const VkDeviceSize bytes = sizeof(float) * cases.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);
}
VkDescriptorSetLayoutBinding bind{};
bind.binding = 0;
bind.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
bind.descriptorCount = 1;
bind.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
VkDescriptorSetLayoutCreateInfo dli{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
dli.bindingCount = 1; dli.pBindings = &bind;
VkDescriptorSetLayout dsl;
VK(vkCreateDescriptorSetLayout(c.dev, &dli, nullptr, &dsl));
VkPipelineLayoutCreateInfo pli{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
pli.setLayoutCount = 1; pli.pSetLayouts = &dsl;
VkPipelineLayout playout;
VK(vkCreatePipelineLayout(c.dev, &pli, nullptr, &playout));
VkShaderModuleCreateInfo smi{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
smi.codeSize = spv.size() * 4; smi.pCode = spv.data();
VkShaderModule mod;
VK(vkCreateShaderModule(c.dev, &smi, nullptr, &mod));
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, 1};
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 = &dsl;
VkDescriptorSet dset;
VK(vkAllocateDescriptorSets(c.dev, &dai, &dset));
VkDescriptorBufferInfo dbi{buf, 0, VK_WHOLE_SIZE};
VkWriteDescriptorSet w{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
w.dstSet = dset; w.dstBinding = 0; w.descriptorCount = 1;
w.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; w.pBufferInfo = &dbi;
vkUpdateDescriptorSets(c.dev, 1, &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, 1, 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 last_op = -1;
for (auto &k : cases) {
const double g = got[k.idx];
const double want = k.cpu;
const double diff = fabs(g - want);
const double scale = fabs(want) > 1.0 ? fabs(want) : 1.0;
const bool nan_both = std::isnan(g) && std::isnan(want);
const bool ok = nan_both || diff <= TOL_ABS || (diff / scale) <= TOL_REL;
if (!ok) {
bad++;
printf(" FAIL %s op %-2d a=%-12g b=%-12g c=%-12g cpu=%-14g gpu=%-14g d=%g\n",
k.node.c_str(), k.op, k.a, k.b, k.c, want, g, diff);
}
last_op = k.op;
}
(void)last_op;
printf("\n%d/%zu cases agree\n", (int)cases.size() - bad, cases.size());
return bad ? 1 : 0;
}