// ============================================================================
// 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;
}