OverviewFilesCommitsIssuesDocsReleasesPeers

dc-terrain / tests / test_edit_contract.cpp

506 lines · cpp ref:
// ============================================================================
// EDIT CONTRACT ORACLE — does the MESH stage sample the same carved field the
// DENSITY stage does, for ANY generator body?
//
// WHAT IT EXISTS TO CATCH.
//
// There are two shader templates and they had OPPOSITE contracts:
//
//   density stage   dc_shader_source.h  _build_custom_shader = HEADER + body +
//                   MAIN, and MAIN applies the edits (:429,:436,:452-456).
//                   => the body must NOT apply them.
//   mesh stage      shaders/edge_intersect.glsl injects the body and calls
//                   evaluate_density() directly for the binary-search midpoints
//                   (:199) and the central-difference normals (:217-219). Its
//                   only edit loop (:249) filters edit_type != 8/9 -- convex
//                   brushes for the exact-Hermite override -- and never touches
//                   the field.
//                   => the body MUST apply them.
//
// The built-in generator satisfies both ONLY because it exists twice:
// DENSITY_GLSL_BODY (no apply) and shaders/generators/noise_sdf.glsl (applies
// inline). noise_sdf even writes the fact down: "Keeping this here (and not
// removing it) is required: edge_intersect.glsl calls evaluate_density()
// directly and never applies edits separately."
//
// A CUSTOM generator has ONE body for BOTH templates and cannot satisfy both.
// DCTerrainGeneratorProcNodes applies edits in neither -- it contains no
// occurrence of `edit`, `apply_gpu_edits` or `apply_brick_delta` at all -- so on
// map_dc_erosion the density grid is carved while the mesh stage's crossing
// search and normals ride the UNCARVED field. That is the failure class
// edge_intersect.glsl:100-105 already documents from a previous incident:
// "per-chunk steps, dark seams and mis-snapped cut floors".
//
// And the tempting small fix -- emit noise_sdf's tail from the proc-nodes
// generator -- is WRONG on the merits: it would double-apply in the density
// stage, and apply_gpu_edits is ADDITIVE (`d += e.strength * t * t`,
// dc_shader_source.h:347), not idempotent. The carve would go twice as deep.
//
// WHAT THIS ORACLE'S SUBJECT ACTUALLY IS -- read before believing a number.
//
//   THE SHADER IS THE SHIPPING ONE. shaders/edge_intersect.glsl is read off
//   disk verbatim, including its main(), its declarations and its
//   GENERATOR_SDF_BODY injection point. Nothing is reconstructed.
//
//   THE BODY IS TRIVIAL, ON PURPOSE: `return -y`, a plane at y=0. That makes
//   the expected answer ANALYTIC, so the oracle cannot be gated by the terrain
//   -- the trap hyper-games #43 6 paid four rounds for. It also stands in for
//   exactly the case that is broken: a generator body that does not apply
//   edits, which is every custom generator.
//
//   THE UPSTREAM BUFFERS ARE HAND-FED. active_edge_indices, counts, edge_scan
//   and the two density endpoints are inputs to this stage, so supplying them
//   directly is legitimate; it is not a substitute for the stage under test.
//   The density endpoints are filled with the CARVED field, because that is
//   what the density stage really produces -- the whole point is that the two
//   stages disagree.
//
//   ONE TRANSCRIPTION, and it is three lines: the sphere branch of
//   apply_gpu_edits (dc_shader_source.h:345-349) is restated in C++ as the
//   analytic reference. Annotated below. If someone changes the falloff, this
//   oracle goes red for the right reason.
//
// Build (no engine, no godot-cpp, no scons):
//   g++ -O2 -std=c++17 -o /tmp/edit_contract tests/test_edit_contract.cpp -lvulkan
//
// Run (from the dc-terrain root, so the shader path resolves):
//   /tmp/edit_contract [--shader PATH]
//
// Exit status 0 when the mesh stage samples the carved field, 1 when it does not.
// ============================================================================

#include <vulkan/vulkan.h>

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <string>
#include <vector>

// ---------------------------------------------------------------------------
// The scene: a plane at y = 0 with one spherical subtract taken out of it.
// ---------------------------------------------------------------------------
static const float EDIT_POS[3]  = {0.0f, 0.0f, 0.0f};
static const float EDIT_RADIUS  = 4.0f;
static const float EDIT_STRENGTH = -4.0f;   // negative = subtract
static const uint32_t EDIT_TYPE = 0u;       // 0 = sphere_sub

// The edge under test. Chosen so the CARVED surface crosses it and the UNCARVED
// one does not come near it -- an uncarved plane sits at y = 0, two metres above
// the far endpoint -- so a stage reading the wrong field cannot land here by
// luck. Offset in x so the carved gradient tilts and the NORMAL discriminates
// too; on the axis both fields give (0,1,0) and the normal would prove nothing.
static const float EDGE_X = 1.5f;
static const float EDGE_Y0 = -2.0f;
static const float VOXEL_SIZE = 1.0f;
static const int   GRID_DIMS = 32;

// The raw generator body. Trivial by design; see the header note.
static const char* TRIVIAL_BODY =
    "// Trivial analytic body: a plane at y = 0, solid below. It applies NO\n"
    "// edits -- exactly like DCTerrainGeneratorProcNodes, and exactly like the\n"
    "// density stage's DENSITY_GLSL_BODY. That is the contract under test.\n"
    "float evaluate_density(float x, float y, float z) {\n"
    "    return -y;\n"
    "}\n";

// --- THE ONE TRANSCRIPTION: apply_gpu_edits' sphere branch -------------------
// dc_shader_source.h:341-349
//     float dist_sq = edx*edx + edy*edy + edz*edz;
//     if (dist_sq > e.radius * e.radius) continue;
//     float t = 1.0 - dist_sq / (radius*radius);
//     d += e.strength * t * t;
static float carved(float x, float y, float z) {
    float d = -y;
    float ex = x - EDIT_POS[0], ey = y - EDIT_POS[1], ez = z - EDIT_POS[2];
    float dist_sq = ex * ex + ey * ey + ez * ez;
    float r_sq = EDIT_RADIUS * EDIT_RADIUS;
    if (dist_sq <= r_sq) {
        float t = 1.0f - dist_sq / r_sq;
        d += EDIT_STRENGTH * t * t;
    }
    return d;
}

static float uncarved(float x, float y, float z) { (void)x; (void)z; return -y; }

// Bisect a field along the test edge. Returns the y of the zero crossing, or
// NAN when the field does not cross it at all.
template <typename F>
static float root_on_edge(F f) {
    float lo = EDGE_Y0, hi = EDGE_Y0 + VOXEL_SIZE;
    float dlo = f(EDGE_X, lo, 0.0f), dhi = f(EDGE_X, hi, 0.0f);
    if ((dlo >= 0.0f) == (dhi >= 0.0f)) return NAN;
    for (int i = 0; i < 60; i++) {
        float m = 0.5f * (lo + hi);
        float dm = f(EDGE_X, m, 0.0f);
        if ((dlo >= 0.0f) == (dm >= 0.0f)) { lo = m; dlo = dm; } else { hi = m; }
    }
    return 0.5f * (lo + hi);
}

static void carved_normal(float x, float y, float z, float out[3]) {
    const float h = VOXEL_SIZE * 0.01f;
    float nx = carved(x + h, y, z) - carved(x - h, y, z);
    float ny = carved(x, y + h, z) - carved(x, y - h, z);
    float nz = carved(x, y, z + h) - carved(x, y, z - h);
    float len = std::sqrt(nx * nx + ny * ny + nz * nz);
    if (len > 1e-8f) { nx /= len; ny /= len; nz /= len; }
    out[0] = nx; out[1] = ny; out[2] = nz;
}

// ---------------------------------------------------------------------------
// Vulkan scaffolding. Mirrors tests/test_erosion_parity.cpp rather than
// factoring a shared harness out of it: refactoring the one working GPU
// instrument in the repo in order to land a test is how you end up with neither.
// ---------------------------------------------------------------------------
static void die(const char* what, VkResult r) {
    fprintf(stderr, "FATAL: %s -> VkResult %d\n", what, (int)r);
    exit(1);
}
#define VK(x) do { VkResult _r = (x); if (_r != VK_SUCCESS) die(#x, _r); } while (0)

static std::string read_file(const char* path) {
    FILE* f = fopen(path, "rb");
    if (!f) { fprintf(stderr, "FATAL: cannot open %s\n", path); exit(1); }
    fseek(f, 0, SEEK_END); long n = ftell(f); fseek(f, 0, SEEK_SET);
    std::string s(n, '\0');
    if (fread(&s[0], 1, n, f) != (size_t)n) { fprintf(stderr, "FATAL: short read\n"); exit(1); }
    fclose(f);
    return s;
}

static std::vector<uint32_t> compile_glsl(const std::string& src, const char* tag) {
    char in_path[] = "/tmp/dc_edit_contract_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_path = std::string(in_path) + ".spv";
    std::string cmd = "glslc -fshader-stage=comp -O -o " + spv_path + " " + in_path;
    if (system(cmd.c_str()) != 0) {
        fprintf(stderr, "FATAL: glslc failed for %s (source left at %s)\n", tag, in_path);
        exit(1);
    }
    FILE* sf = fopen(spv_path.c_str(), "rb");
    if (!sf) { fprintf(stderr, "FATAL: no SPIR-V at %s\n", spv_path.c_str()); exit(1); }
    fseek(sf, 0, SEEK_END); long n = ftell(sf); fseek(sf, 0, SEEK_SET);
    std::vector<uint32_t> spv(n / 4);
    if (fread(spv.data(), 1, n, sf) != (size_t)n) { fprintf(stderr, "FATAL: short read\n"); exit(1); }
    fclose(sf);
    remove(in_path); remove(spv_path.c_str());
    return spv;
}

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);
}

struct Buf { VkBuffer buf = VK_NULL_HANDLE; VkDeviceMemory mem = VK_NULL_HANDLE;
             void* mapped = nullptr; VkDeviceSize size = 0; };

static Buf make_host_buf(Ctx& c, VkDeviceSize size, VkBufferUsageFlags usage) {
    if (size == 0) size = 16;
    Buf b; b.size = size;
    VkBufferCreateInfo bi{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
    bi.size = size; bi.usage = usage; bi.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
    VK(vkCreateBuffer(c.dev, &bi, nullptr, &b.buf));
    VkMemoryRequirements mr;
    vkGetBufferMemoryRequirements(c.dev, b.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, &b.mem));
    VK(vkBindBufferMemory(c.dev, b.buf, b.mem, 0));
    VK(vkMapMemory(c.dev, b.mem, 0, VK_WHOLE_SIZE, 0, &b.mapped));
    memset(b.mapped, 0, (size_t)size);
    return b;
}

static void init_vulkan(Ctx& c) {
    VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO};
    app.pApplicationName = "dc-terrain edit contract oracle";
    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));
}

// --- push constants, matching edge_intersect.glsl's block exactly ------------
struct PC {
    float cell_origin[3]; float voxel_size;
    int32_t grid_dims[3]; uint32_t total_elements;
    uint32_t mesh_quality_flags; uint32_t pad0, pad1, pad2;
};

// --- GpuEdit, matching the std430 struct in edge_intersect.glsl --------------
struct GpuEdit {
    uint32_t edit_type;
    float pos_x, pos_y, pos_z;
    float half_x, half_y, half_z;
    float radius, strength;
    uint32_t pad0, pad1, pad2;
};

int main(int argc, char** argv) {
    const char* shader_path = "shaders/edge_intersect.glsl";
    for (int i = 1; i < argc; i++)
        if (!strcmp(argv[i], "--shader") && i + 1 < argc) shader_path = argv[++i];

    // ---- Build the shader: shipping template, trivial body -----------------
    std::string tmpl = read_file(shader_path);
    const std::string marker = "// GENERATOR_SDF_BODY";
    size_t mpos = tmpl.find(marker);
    if (mpos == std::string::npos) {
        fprintf(stderr, "FATAL: GENERATOR_SDF_BODY marker not found in %s\n", shader_path);
        return 1;
    }
    tmpl.replace(mpos, marker.length(), TRIVIAL_BODY);

    printf("edit-contract oracle\n");
    printf("  shader   %s (%zu bytes, main() and all declarations verbatim)\n",
           shader_path, tmpl.size());

    // ---- The analytic answers ---------------------------------------------
    float y_carved = root_on_edge(carved);
    float y_uncarved = root_on_edge(uncarved);
    if (std::isnan(y_carved)) {
        fprintf(stderr, "FATAL: the carved field does not cross the test edge -- "
                        "the SCENE is wrong, not the shader.\n");
        return 1;
    }
    float n_ref[3];
    carved_normal(EDGE_X, y_carved, 0.0f, n_ref);
    printf("  edge     (%.2f, %.2f, 0) -> (%.2f, %.2f, 0)\n",
           EDGE_X, EDGE_Y0, EDGE_X, EDGE_Y0 + VOXEL_SIZE);
    printf("  carved   crossing at y = %+.5f, normal (%.4f, %.4f, %.4f)\n",
           y_carved, n_ref[0], n_ref[1], n_ref[2]);
    if (std::isnan(y_uncarved))
        printf("  uncarved field does NOT cross this edge at all (plane is at y = 0)\n");
    else
        printf("  uncarved crossing at y = %+.5f\n", y_uncarved);

    Ctx c{};
    init_vulkan(c);
    printf("  device   %s\n\n", c.devname);

    std::vector<uint32_t> spv = compile_glsl(tmpl, "edge_intersect + trivial body");

    // ---- Buffers, one per declared binding ---------------------------------
    const int density_dim = GRID_DIMS + 1;                 // 33
    const size_t density_n = (size_t)density_dim * density_dim * density_dim;
    const size_t edge_n = (size_t)GRID_DIMS * GRID_DIMS * GRID_DIMS * 3;

    const VkBufferUsageFlags SSBO = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
    const VkBufferUsageFlags UBO  = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;

    Buf b_density = make_host_buf(c, density_n * 4, SSBO);
    Buf b_flags   = make_host_buf(c, edge_n * 4, SSBO);
    Buf b_scan    = make_host_buf(c, edge_n * 4, SSBO);
    Buf b_out     = make_host_buf(c, 64 * 4, SSBO);
    Buf b_params  = make_host_buf(c, 256, UBO);
    Buf b_voronoi = make_host_buf(c, 256, SSBO);
    Buf b_perm    = make_host_buf(c, 3072 * 4, SSBO);
    Buf b_edits   = make_host_buf(c, 16 + sizeof(GpuEdit), SSBO);
    Buf b_active  = make_host_buf(c, 64 * 4, SSBO);
    Buf b_counts  = make_host_buf(c, 64 * 4, SSBO);
    Buf b_brick   = make_host_buf(c, density_n * 4, SSBO);
    Buf b_planes  = make_host_buf(c, 64 * 16, SSBO);

    // The one active edge: voxel (0,0,0), dir 1 (the Y edge).
    const uint32_t dir = 1u;
    const uint32_t gid = 0u * 3u + dir;
    ((uint32_t*)b_active.mapped)[0] = gid;
    ((uint32_t*)b_counts.mapped)[0] = 1u;
    ((uint32_t*)b_scan.mapped)[gid] = 0u;

    // Density endpoints = the CARVED field, because that is what the density
    // stage really produces. The disagreement between the two stages is the
    // whole subject.
    const int d_idx0 = 0;
    const int d_idx1 = d_idx0 + density_dim;   // dir == 1 steps by density_dim
    ((float*)b_density.mapped)[d_idx0] = carved(EDGE_X, EDGE_Y0, 0.0f);
    ((float*)b_density.mapped)[d_idx1] = carved(EDGE_X, EDGE_Y0 + VOXEL_SIZE, 0.0f);

    // One spherical subtract. flags = 0, so the brick path is off and the
    // inline/edit path is the one under test.
    ((uint32_t*)b_edits.mapped)[0] = 1u;
    GpuEdit* ge = (GpuEdit*)((char*)b_edits.mapped + 16);
    memset(ge, 0, sizeof(*ge));
    ge->edit_type = EDIT_TYPE;
    ge->pos_x = EDIT_POS[0]; ge->pos_y = EDIT_POS[1]; ge->pos_z = EDIT_POS[2];
    ge->radius = EDIT_RADIUS;
    ge->strength = EDIT_STRENGTH;

    // ---- Descriptors -------------------------------------------------------
    const int NB = 12;
    VkDescriptorSetLayoutBinding binds[NB]{};
    for (int i = 0; i < NB; i++) {
        binds[i].binding = i;
        binds[i].descriptorType = (i == 4) ? VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER
                                           : VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
        binds[i].descriptorCount = 1;
        binds[i].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
    }
    VkDescriptorSetLayoutCreateInfo dli{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
    dli.bindingCount = NB; dli.pBindings = binds;
    VkDescriptorSetLayout dsl;
    VK(vkCreateDescriptorSetLayout(c.dev, &dli, nullptr, &dsl));

    VkPushConstantRange pcr{VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(PC)};
    VkPipelineLayoutCreateInfo pli{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
    pli.setLayoutCount = 1; pli.pSetLayouts = &dsl;
    pli.pushConstantRangeCount = 1; pli.pPushConstantRanges = &pcr;
    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));

    VkComputePipelineCreateInfo cpi{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO};
    cpi.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
    cpi.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT;
    cpi.stage.module = mod; cpi.stage.pName = "main";
    cpi.layout = playout;
    VkPipeline pipe;
    VK(vkCreateComputePipelines(c.dev, VK_NULL_HANDLE, 1, &cpi, nullptr, &pipe));

    VkDescriptorPoolSize ps[2] = {
        {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, NB - 1},
        {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1},
    };
    VkDescriptorPoolCreateInfo dpi{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
    dpi.maxSets = 1; dpi.poolSizeCount = 2; 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));

    Buf* order[NB] = { &b_density, &b_flags, &b_scan, &b_out, &b_params,
                       &b_voronoi, &b_perm, &b_edits, &b_active, &b_counts,
                       &b_brick, &b_planes };
    VkDescriptorBufferInfo dbi[NB];
    VkWriteDescriptorSet w[NB]{};
    for (int i = 0; i < NB; i++) {
        dbi[i] = { order[i]->buf, 0, VK_WHOLE_SIZE };
        w[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
        w[i].dstSet = dset; w[i].dstBinding = i; w[i].descriptorCount = 1;
        w[i].descriptorType = binds[i].descriptorType;
        w[i].pBufferInfo = &dbi[i];
    }
    vkUpdateDescriptorSets(c.dev, NB, w, 0, nullptr);

    PC pc{};
    pc.cell_origin[0] = EDGE_X; pc.cell_origin[1] = EDGE_Y0; pc.cell_origin[2] = 0.0f;
    pc.voxel_size = VOXEL_SIZE;
    pc.grid_dims[0] = pc.grid_dims[1] = pc.grid_dims[2] = GRID_DIMS;
    pc.total_elements = 1;
    pc.mesh_quality_flags = 0;

    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 cbb{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
    cbb.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
    VK(vkBeginCommandBuffer(cmd, &cbb));
    vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, pipe);
    vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, playout, 0, 1, &dset, 0, nullptr);
    vkCmdPushConstants(cmd, playout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(PC), &pc);
    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*)b_out.mapped;
    float px = got[0], py = got[1], pz = got[2];
    float nx = got[3], ny = got[4], nz = got[5];

    float dy = py - y_carved;
    float dot = nx * n_ref[0] + ny * n_ref[1] + nz * n_ref[2];
    if (dot > 1.0f) dot = 1.0f;
    if (dot < -1.0f) dot = -1.0f;
    float nerr = std::acos(dot) * 57.2957795f;

    printf("mesh stage returned\n");
    printf("  position   (%+.5f, %+.5f, %+.5f)\n", px, py, pz);
    printf("  normal     (%.4f, %.4f, %.4f)\n", nx, ny, nz);
    printf("\n");
    printf("  crossing error vs carved surface   %+.5f m\n", dy);
    printf("  normal error vs carved gradient    %6.2f deg\n", nerr);
    printf("\n");

    bool ok = (std::fabs(dy) < 0.01f) && (nerr < 3.0f);
    if (ok) {
        printf("PASS  the mesh stage samples the carved field for a body that "
               "does not apply edits\n");
        return 0;
    }
    printf("FAIL  the mesh stage is NOT sampling the carved field.\n");
    printf("      evaluate_density() returns the RAW field for this body, and\n");
    printf("      edge_intersect.glsl applies no edits of its own, so the\n");
    printf("      crossing search and the normals ride an uncarved surface\n");
    printf("      while the density endpoints are carved. This is what breaks\n");
    printf("      CSG on every custom generator (map_dc_erosion).\n");
    return 1;
}