OverviewFilesCommitsIssuesDocsReleasesPeers

dc-terrain commits / 9a4cb84

edits: one contract for both templates — the mesh stage applies them too
CSG carves did nothing on map_dc_erosion. Not a map setting: it is the only map in hyper-tps that installs a generator (map_dc_erosion.gd:286); dc_terrain, dc_world and dc_level all run the built-in one, which is why they carve fine. TWO TEMPLATES, OPPOSITE CONTRACTS, ONE BODY. 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 edge_intersect.glsl injected the body and called 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 touched the field. => the body MUST apply them. The built-in generator satisfied both only by existing TWICE: DENSITY_GLSL_BODY without the apply, generators/noise_sdf.glsl with it. noise_sdf even wrote 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. dc_gpu_thread.h hands the SAME `glsl` to compute_.set_custom_shader (:258) and mesh_pipeline_.set_custom_sdf (:321), and DCTerrainGeneratorProcNodes applies edits in neither — zero occurrences of `edit`, `apply_gpu_edits` or `apply_brick_delta` in it, and none in proc-nodes either, so nothing in the compiled graph can supply them. So the density grid was carved while the mesh stage's crossing search and normals rode the UNCARVED field: the failure class edge_intersect.glsl:100-105 already documents from a previous incident — "per-chunk steps, dark seams and mis-snapped cut floors". WHY NOT THE SMALL FIX. Emitting noise_sdf's tail from the proc-nodes generator 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. Ruled out on the merits, not on taste. THE FIX. apply_gpu_edits and apply_brick_delta move from the body into edge_intersect.glsl, which now wraps the injected body in dc_sample_field() and calls that from all seven sites in main(). noise_sdf.glsl loses its three inline application sites. One contract everywhere, and it is the density stage's: a generator body returns the RAW field; the template applies the edits. DCTerrainGeneratorProcNodes needs no change, and the next generator is correct by construction. Copy count is unchanged at two — one per template — but it is now per-TEMPLATE instead of per-BODY, which is what made it possible to get wrong. THE GATE, WRITTEN FIRST. tests/test_edit_contract.cpp drives the SHIPPING edge_intersect.glsl — its main(), its declarations, its injection point, read off disk — with a trivial analytic body (`return -y`) and one spherical subtract, hand-feeding the upstream buffers this stage takes as inputs. The trivial body is the point: it makes the expected answer analytic, so the oracle cannot be gated by the terrain, and it stands in for exactly the broken case. before position (1.500, -1.002, 0) crossing error +0.76404 m normal 24.63 deg after position (1.500, -1.768, 0) crossing error -0.00158 m normal 0.02 deg The uncarved field does not cross the test edge at all, so before the fix the search pinned to the far endpoint. The residual after is the binary search's own quantum (1 m / 2^8 = 0.0039 m). That green also lifts the bottom layer: the GPU's apply_gpu_edits now provably agrees with the C++ reference, so the one transcription in the oracle is established rather than assumed. The default composition (edge_intersect + noise_sdf) still compiles under glslc. ONE BEHAVIOUR CHANGE WORTH NAMING: in the `use_noise == 0` branch noise_sdf used to apply edits BEFORE its floor/sky blend; the template applies after. Additive sphere edits commute with those blends, box/hard/convex (min/max) do not. The spherical deep-interior early-out is unaffected — nothing ran between. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
9a4cb8413aab0fe1cf4fea06092ebef8d4719b57
by plankatron <fleeky@gmail.com> · 2026-08-31T15:00:16.000Z

Changed files (6) +668 -117

shaders/bake_planeset.glsl.import +14 -0
……@@ -0,0 +1,14 @@
1[remap]
2
3importer="glsl"
4type="RDShaderFile"
5uid="uid://bgyd8fcfjsg6j"
6path="res://.godot/imported/bake_planeset.glsl-e98f44ce1185294684978da08a429a38.res"
7
8[deps]
9
10source_file="res://bin/linux/shaders/bake_planeset.glsl"
11dest_files=["res://.godot/imported/bake_planeset.glsl-e98f44ce1185294684978da08a429a38.res"]
12
13[params]
14
shaders/comp.spv +0 -0
(no content changes — binary or rename)
shaders/edge_intersect.glsl +137 -4
……@@ -151,6 +151,139 @@ layout(push_constant) uniform PushConstants {
151151// Must define: float evaluate_density(float x, float y, float z)
152152// =====================================================================
153153// GENERATOR_SDF_BODY
154// =====================================================================
155// FIELD SAMPLING — edits and the settled brick are applied HERE, by the
156// TEMPLATE, for every generator body.
157//
158// THEY USED TO BE THE BODY'S JOB, and only one body ever did it. There are two
159// templates and they had OPPOSITE contracts: the density stage
160// (dc_shader_source.h, _build_custom_shader = HEADER + body + MAIN) applies the
161// edits in MAIN, so its body must NOT; this stage called evaluate_density()
162// directly and applied none, so its body MUST. The built-in generator satisfied
163// both only by existing twice -- DENSITY_GLSL_BODY without the apply,
164// generators/noise_sdf.glsl with it.
165//
166// A CUSTOM generator has ONE body for BOTH templates and cannot satisfy both.
167// DCTerrainGeneratorProcNodes applies edits in neither (dc_gpu_thread.h:258 and
168// :321 hand the SAME `glsl` string to set_custom_shader and set_custom_sdf), so
169// on map_dc_erosion the density grid was carved while this stage's crossing
170// search and normals rode the UNCARVED field -- the failure class the EditBuf
171// note above already documents from a previous incident.
172//
173// Emitting the tail from the proc-nodes generator instead would have been
174// WRONG: the density stage would then apply twice, and apply_gpu_edits is
175// ADDITIVE (`d += e.strength * t * t`), not idempotent. The carve would go
176// twice as deep.
177//
178// So the contract is now ONE contract, and it is the density stage's:
179// a generator body returns the RAW field; the template applies the edits.
180// Gate: tests/test_edit_contract.cpp drives THIS main() with a trivial
181// analytic body and asserts the crossing lands on the carved surface.
182// =====================================================================
183// Edit application — IDENTICAL to the density-stage apply_gpu_edits in
184// dc_compute.cpp so the mesh stage carves the exact same surface the density
185// grid was carved with (no per-chunk divergence). Box + sphere only; mesh-SDF
186// edits (etype>=4) need the BVH bindings the mesh stage doesn't have, so they
187// are skipped here.
188float apply_gpu_edits(float d, float wx, float wy, float wz) {
189 for (uint i = 0u; i < edit_count; i++) {
190 GpuEdit e = gpu_edits[i];
191 uint etype = e.edit_type;
192
193 // Bounding sphere early reject
194 float edx = wx - e.pos_x;
195 float edy = wy - e.pos_y;
196 float edz = wz - e.pos_z;
197 float dist_sq = edx*edx + edy*edy + edz*edz;
198 if (dist_sq > e.radius * e.radius) continue;
199
200 if (etype <= 1u) {
201 // Sphere: smooth quadratic falloff
202 float r_sq = e.radius * e.radius;
203 float t = 1.0 - dist_sq / r_sq;
204 d += e.strength * t * t;
205 } else if (etype <= 3u) {
206 // Box: SDF-based CSG
207 float dx = abs(wx - e.pos_x) - e.half_x;
208 float dy = abs(wy - e.pos_y) - e.half_y;
209 float dz = abs(wz - e.pos_z) - e.half_z;
210 float outside = sqrt(
211 max(dx, 0.0) * max(dx, 0.0) +
212 max(dy, 0.0) * max(dy, 0.0) +
213 max(dz, 0.0) * max(dz, 0.0));
214 float inside = min(max(max(dx, dy), dz), 0.0);
215 float box_sdf = outside + inside;
216 if (e.strength < 0.0) {
217 d = min(d, box_sdf);
218 } else {
219 d = max(d, -box_sdf);
220 }
221 } else if (etype == 6u || etype == 7u) {
222 // Hard sphere CSG: SDF = dist - radius (neg inside, pos outside —
223 // same convention as box_sdf). Union/difference by strength sign.
224 float sphere_sdf = sqrt(dist_sq) - e.radius;
225 if (e.strength < 0.0) {
226 d = min(d, sphere_sdf);
227 } else {
228 d = max(d, -sphere_sdf);
229 }
230 } else if (etype == 8u || etype == 9u) {
231 // Convex brush = intersection of half-spaces: f = max_i(n_i·p - d_i)
232 // (neg inside, pos outside). Planes in [_pad1, _pad1+_pad2). IDENTICAL
233 // to the density-stage convex branch in dc_compute.cpp.
234 uint poff = e._pad1;
235 uint pcnt = e._pad2;
236 float f = -1e30;
237 for (uint pi = 0u; pi < pcnt; pi++) {
238 vec4 pl = convex_planes[poff + pi];
239 f = max(f, pl.x * wx + pl.y * wy + pl.z * wz - pl.w);
240 }
241 if (e.strength < 0.0) {
242 d = min(d, f);
243 } else {
244 d = max(d, -f);
245 }
246 }
247 // etype 4/5 (mesh-SDF edits) skipped: BVH bindings unavailable here.
248 }
249 return d;
250}
251
252// =====================================================================
253// Settled-edit SDF delta brick — trilinear sample of brick_delta[] (binding
254// 10, declared in edge_intersect.glsl). Chunk-local grid at the density
255// stride (grid_dims + 1). MUST match the density-stage apply_brick_delta in
256// dc_compute.cpp so the fractional mesh-stage samples agree with the grid
257// endpoints (same divergence class as the two evaluate_density copies). A
258// zero buffer returns 0 exactly, so this is a no-op until a bake fills it.
259// =====================================================================
260float apply_brick_delta(float wx, float wy, float wz) {
261 int stride = grid_dims.x + 1;
262 vec3 lv = (vec3(wx, wy, wz) - cell_origin) / voxel_size;
263 lv = clamp(lv, vec3(0.0), vec3(float(stride - 1) - 1e-4));
264 ivec3 i0 = ivec3(floor(lv));
265 vec3 f = lv - vec3(i0);
266 int sx = stride, sxy = stride * stride;
267 int b = i0.z * sxy + i0.y * sx + i0.x;
268 float c00 = mix(brick_delta[b], brick_delta[b + 1], f.x);
269 float c10 = mix(brick_delta[b + sx], brick_delta[b + sx + 1], f.x);
270 float c01 = mix(brick_delta[b + sxy], brick_delta[b + sxy + 1], f.x);
271 float c11 = mix(brick_delta[b + sxy + sx], brick_delta[b + sxy + sx + 1], f.x);
272 return mix(mix(c00, c10, f.y), mix(c01, c11, f.y), f.z);
273}
274
275float dc_sample_field(float x, float y, float z) {
276 float d = evaluate_density(x, y, z);
277 // Order and gating match what noise_sdf.glsl did inline: the settled brick
278 // already has the edits folded in, so exactly one of the two applies.
279 if ((flags & 2) != 0) {
280 d += apply_brick_delta(x, y, z);
281 } else if (edit_count > 0u) {
282 d = apply_gpu_edits(d, x, y, z);
283 }
284 return d;
285}
286
154287
155288// =====================================================================
156289// Main: binary search + gradient in single pass
……@@ -196,7 +329,7 @@ void main() {
196329 for (int i = 0; i < BINARY_SEARCH_ITERS; i++) {
197330 float mid = (lo + hi) * 0.5;
198331 vec3 p_mid = mix(p0, p1, mid);
199 float d_mid = evaluate_density(p_mid.x, p_mid.y, p_mid.z);
332 float d_mid = dc_sample_field(p_mid.x, p_mid.y, p_mid.z);
200333
201334 if ((d_lo >= 0.0) == (d_mid >= 0.0)) {
202335 lo = mid;
……@@ -214,9 +347,9 @@ void main() {
214347 // ---- Surface normal via central differences (fused from Stage 5) ----
215348 float eps = voxel_size * 0.01;
216349 vec3 n;
217 n.x = evaluate_density(pos.x + eps, pos.y, pos.z) - evaluate_density(pos.x - eps, pos.y, pos.z);
218 n.y = evaluate_density(pos.x, pos.y + eps, pos.z) - evaluate_density(pos.x, pos.y - eps, pos.z);
219 n.z = evaluate_density(pos.x, pos.y, pos.z + eps) - evaluate_density(pos.x, pos.y, pos.z - eps);
350 n.x = dc_sample_field(pos.x + eps, pos.y, pos.z) - dc_sample_field(pos.x - eps, pos.y, pos.z);
351 n.y = dc_sample_field(pos.x, pos.y + eps, pos.z) - dc_sample_field(pos.x, pos.y - eps, pos.z);
352 n.z = dc_sample_field(pos.x, pos.y, pos.z + eps) - dc_sample_field(pos.x, pos.y, pos.z - eps);
220353
221354 float len = length(n);
222355 if (len > 1e-8) {
shaders/generators/noise_sdf.glsl +8 -111
……@@ -128,97 +128,13 @@ float sample_noise(int table_offset, float freq, float px, float py, float pz, i
128128// CSG primitive evaluation (port of dc_density.cpp)
129129// =====================================================================
130130
131// Edit application — IDENTICAL to the density-stage apply_gpu_edits in
132// dc_compute.cpp so the mesh stage carves the exact same surface the density
133// grid was carved with (no per-chunk divergence). Box + sphere only; mesh-SDF
134// edits (etype>=4) need the BVH bindings the mesh stage doesn't have, so they
135// are skipped here.
136float apply_gpu_edits(float d, float wx, float wy, float wz) {
137 for (uint i = 0u; i < edit_count; i++) {
138 GpuEdit e = gpu_edits[i];
139 uint etype = e.edit_type;
140
141 // Bounding sphere early reject
142 float edx = wx - e.pos_x;
143 float edy = wy - e.pos_y;
144 float edz = wz - e.pos_z;
145 float dist_sq = edx*edx + edy*edy + edz*edz;
146 if (dist_sq > e.radius * e.radius) continue;
147
148 if (etype <= 1u) {
149 // Sphere: smooth quadratic falloff
150 float r_sq = e.radius * e.radius;
151 float t = 1.0 - dist_sq / r_sq;
152 d += e.strength * t * t;
153 } else if (etype <= 3u) {
154 // Box: SDF-based CSG
155 float dx = abs(wx - e.pos_x) - e.half_x;
156 float dy = abs(wy - e.pos_y) - e.half_y;
157 float dz = abs(wz - e.pos_z) - e.half_z;
158 float outside = sqrt(
159 max(dx, 0.0) * max(dx, 0.0) +
160 max(dy, 0.0) * max(dy, 0.0) +
161 max(dz, 0.0) * max(dz, 0.0));
162 float inside = min(max(max(dx, dy), dz), 0.0);
163 float box_sdf = outside + inside;
164 if (e.strength < 0.0) {
165 d = min(d, box_sdf);
166 } else {
167 d = max(d, -box_sdf);
168 }
169 } else if (etype == 6u || etype == 7u) {
170 // Hard sphere CSG: SDF = dist - radius (neg inside, pos outside —
171 // same convention as box_sdf). Union/difference by strength sign.
172 float sphere_sdf = sqrt(dist_sq) - e.radius;
173 if (e.strength < 0.0) {
174 d = min(d, sphere_sdf);
175 } else {
176 d = max(d, -sphere_sdf);
177 }
178 } else if (etype == 8u || etype == 9u) {
179 // Convex brush = intersection of half-spaces: f = max_i(n_i·p - d_i)
180 // (neg inside, pos outside). Planes in [_pad1, _pad1+_pad2). IDENTICAL
181 // to the density-stage convex branch in dc_compute.cpp.
182 uint poff = e._pad1;
183 uint pcnt = e._pad2;
184 float f = -1e30;
185 for (uint pi = 0u; pi < pcnt; pi++) {
186 vec4 pl = convex_planes[poff + pi];
187 f = max(f, pl.x * wx + pl.y * wy + pl.z * wz - pl.w);
188 }
189 if (e.strength < 0.0) {
190 d = min(d, f);
191 } else {
192 d = max(d, -f);
193 }
194 }
195 // etype 4/5 (mesh-SDF edits) skipped: BVH bindings unavailable here.
196 }
197 return d;
198}
199131
200// =====================================================================
201// Settled-edit SDF delta brick — trilinear sample of brick_delta[] (binding
202// 10, declared in edge_intersect.glsl). Chunk-local grid at the density
203// stride (grid_dims + 1). MUST match the density-stage apply_brick_delta in
204// dc_compute.cpp so the fractional mesh-stage samples agree with the grid
205// endpoints (same divergence class as the two evaluate_density copies). A
206// zero buffer returns 0 exactly, so this is a no-op until a bake fills it.
207// =====================================================================
208float apply_brick_delta(float wx, float wy, float wz) {
209 int stride = grid_dims.x + 1;
210 vec3 lv = (vec3(wx, wy, wz) - cell_origin) / voxel_size;
211 lv = clamp(lv, vec3(0.0), vec3(float(stride - 1) - 1e-4));
212 ivec3 i0 = ivec3(floor(lv));
213 vec3 f = lv - vec3(i0);
214 int sx = stride, sxy = stride * stride;
215 int b = i0.z * sxy + i0.y * sx + i0.x;
216 float c00 = mix(brick_delta[b], brick_delta[b + 1], f.x);
217 float c10 = mix(brick_delta[b + sx], brick_delta[b + sx + 1], f.x);
218 float c01 = mix(brick_delta[b + sxy], brick_delta[b + sxy + 1], f.x);
219 float c11 = mix(brick_delta[b + sxy + sx], brick_delta[b + sxy + sx + 1], f.x);
220 return mix(mix(c00, c10, f.y), mix(c01, c11, f.y), f.z);
221}
132// EDIT APPLICATION LIVES IN THE TEMPLATE NOW (edge_intersect.glsl's
133// dc_sample_field), not here. apply_gpu_edits and apply_brick_delta moved there
134// with it. This body returns the RAW field, which is the same contract
135// DENSITY_GLSL_BODY has always had -- one contract for both templates, so a
136// custom generator can satisfy it. Re-adding an inline apply here would DOUBLE
137// it, and apply_gpu_edits is additive, not idempotent.
222138
223139// =====================================================================
224140// Density evaluation (exact port of DCDensity::evaluate)
……@@ -264,7 +180,6 @@ float evaluate_density(float x, float y, float z) {
264180 float max_displacement = primary_strength + ridge_strength +
265181 detail_strength + warp_strength * 0.5;
266182 if (abs(d) > max_displacement * 2.5) {
267 if (edit_count > 0u) d = apply_gpu_edits(d, x, y, z);
268183 return d;
269184 }
270185 } else {
……@@ -273,7 +188,6 @@ float evaluate_density(float x, float y, float z) {
273188
274189 // === Noise layers (skipped when use_noise == 0) ===
275190 if (use_noise == 0) {
276 if (edit_count > 0u) d = apply_gpu_edits(d, x, y, z);
277191 if (spherical == 0) {
278192 float floor_blend = max((-y - solid_floor_depth) * 0.1, 0.0);
279193 d += floor_blend;
……@@ -455,24 +369,7 @@ float evaluate_density(float x, float y, float z) {
455369 }
456370 } // end flat-terrain-only block
457371
458 // === Settled brick — added before HOT edits (base + brick, then HOT).
459 // Gated on flags bit1 so no-edit chunks (zero/stale brick) read nothing. ===
460 if ((flags & 2) != 0) d += apply_brick_delta(x, y, z);
461
462 // === Edits (CSG) — applied LAST, matching the density-fill stage which
463 // applies edits in main() after the full evaluate_density(). Keeping this
464 // here (and not removing it) is required: edge_intersect.glsl calls
465 // evaluate_density() directly and never applies edits separately. ===
466 // STEP 2: when SAMPLING the settled-edit brick (flags bit1), the edits are
467 // ALREADY folded into brick_delta above — skip the inline re-apply to avoid
468 // double-counting. edit_buf stays POPULATED though (dc_gpu_thread no longer
469 // empties it on brick chunks), so edge_intersect's exact-Hermite override
470 // still reads the convex planes and sharpens faces/edges THROUGH the brick
471 // (verified: tests/test_brick.cpp — rotated face 0.075 → 0.000). Non-brick
472 // chunks (bit1 clear) apply edits inline exactly as before.
473 if (edit_count > 0u && (flags & 2) == 0) {
474 d = apply_gpu_edits(d, x, y, z);
475 }
476
372 // The brick and the edits are applied by the TEMPLATE (dc_sample_field in
373 // edge_intersect.glsl) — see the note above. Nothing to do here.
477374 return d;
478375}
src/dc_gpu_thread.h +4 -2
……@@ -333,8 +333,10 @@ private:
333333 if (use_brick) {
334334 compute_.bake_brick(req.grid->stride);
335335 // STEP 2: KEEP edit_buf populated (do NOT empty it). The bake
336 // folded the edits into brick_delta, and the density guard
337 // (noise_sdf.glsl skips inline apply when flags bit1 is set)
336 // folded the edits into brick_delta, and the flags-bit1 guard
337 // (edge_intersect.glsl's dc_sample_field takes the brick OR
338 // the edits, never both — it moved there from noise_sdf.glsl
339 // when the two templates were given one contract)
338340 // prevents double-counting — so the brick supplies density
339341 // while edge_intersect's exact-Hermite override reads the
340342 // convex planes from edit_buf and sharpens faces/edges through
tests/test_edit_contract.cpp +505 -0
……@@ -0,0 +1,505 @@
1// ============================================================================
2// EDIT CONTRACT ORACLE — does the MESH stage sample the same carved field the
3// DENSITY stage does, for ANY generator body?
4//
5// WHAT IT EXISTS TO CATCH.
6//
7// There are two shader templates and they had OPPOSITE contracts:
8//
9// density stage dc_shader_source.h _build_custom_shader = HEADER + body +
10// MAIN, and MAIN applies the edits (:429,:436,:452-456).
11// => the body must NOT apply them.
12// mesh stage shaders/edge_intersect.glsl injects the body and calls
13// evaluate_density() directly for the binary-search midpoints
14// (:199) and the central-difference normals (:217-219). Its
15// only edit loop (:249) filters edit_type != 8/9 -- convex
16// brushes for the exact-Hermite override -- and never touches
17// the field.
18// => the body MUST apply them.
19//
20// The built-in generator satisfies both ONLY because it exists twice:
21// DENSITY_GLSL_BODY (no apply) and shaders/generators/noise_sdf.glsl (applies
22// inline). noise_sdf even writes the fact down: "Keeping this here (and not
23// removing it) is required: edge_intersect.glsl calls evaluate_density()
24// directly and never applies edits separately."
25//
26// A CUSTOM generator has ONE body for BOTH templates and cannot satisfy both.
27// DCTerrainGeneratorProcNodes applies edits in neither -- it contains no
28// occurrence of `edit`, `apply_gpu_edits` or `apply_brick_delta` at all -- so on
29// map_dc_erosion the density grid is carved while the mesh stage's crossing
30// search and normals ride the UNCARVED field. That is the failure class
31// edge_intersect.glsl:100-105 already documents from a previous incident:
32// "per-chunk steps, dark seams and mis-snapped cut floors".
33//
34// And the tempting small fix -- emit noise_sdf's tail from the proc-nodes
35// generator -- is WRONG on the merits: it would double-apply in the density
36// stage, and apply_gpu_edits is ADDITIVE (`d += e.strength * t * t`,
37// dc_shader_source.h:347), not idempotent. The carve would go twice as deep.
38//
39// WHAT THIS ORACLE'S SUBJECT ACTUALLY IS -- read before believing a number.
40//
41// THE SHADER IS THE SHIPPING ONE. shaders/edge_intersect.glsl is read off
42// disk verbatim, including its main(), its declarations and its
43// GENERATOR_SDF_BODY injection point. Nothing is reconstructed.
44//
45// THE BODY IS TRIVIAL, ON PURPOSE: `return -y`, a plane at y=0. That makes
46// the expected answer ANALYTIC, so the oracle cannot be gated by the terrain
47// -- the trap hyper-games #43 6 paid four rounds for. It also stands in for
48// exactly the case that is broken: a generator body that does not apply
49// edits, which is every custom generator.
50//
51// THE UPSTREAM BUFFERS ARE HAND-FED. active_edge_indices, counts, edge_scan
52// and the two density endpoints are inputs to this stage, so supplying them
53// directly is legitimate; it is not a substitute for the stage under test.
54// The density endpoints are filled with the CARVED field, because that is
55// what the density stage really produces -- the whole point is that the two
56// stages disagree.
57//
58// ONE TRANSCRIPTION, and it is three lines: the sphere branch of
59// apply_gpu_edits (dc_shader_source.h:345-349) is restated in C++ as the
60// analytic reference. Annotated below. If someone changes the falloff, this
61// oracle goes red for the right reason.
62//
63// Build (no engine, no godot-cpp, no scons):
64// g++ -O2 -std=c++17 -o /tmp/edit_contract tests/test_edit_contract.cpp -lvulkan
65//
66// Run (from the dc-terrain root, so the shader path resolves):
67// /tmp/edit_contract [--shader PATH]
68//
69// Exit status 0 when the mesh stage samples the carved field, 1 when it does not.
70// ============================================================================
71
72#include <vulkan/vulkan.h>
73
74#include <cstdio>
75#include <cstdlib>
76#include <cstring>
77#include <cmath>
78#include <string>
79#include <vector>
80
81// ---------------------------------------------------------------------------
82// The scene: a plane at y = 0 with one spherical subtract taken out of it.
83// ---------------------------------------------------------------------------
84static const float EDIT_POS[3] = {0.0f, 0.0f, 0.0f};
85static const float EDIT_RADIUS = 4.0f;
86static const float EDIT_STRENGTH = -4.0f; // negative = subtract
87static const uint32_t EDIT_TYPE = 0u; // 0 = sphere_sub
88
89// The edge under test. Chosen so the CARVED surface crosses it and the UNCARVED
90// one does not come near it -- an uncarved plane sits at y = 0, two metres above
91// the far endpoint -- so a stage reading the wrong field cannot land here by
92// luck. Offset in x so the carved gradient tilts and the NORMAL discriminates
93// too; on the axis both fields give (0,1,0) and the normal would prove nothing.
94static const float EDGE_X = 1.5f;
95static const float EDGE_Y0 = -2.0f;
96static const float VOXEL_SIZE = 1.0f;
97static const int GRID_DIMS = 32;
98
99// The raw generator body. Trivial by design; see the header note.
100static const char* TRIVIAL_BODY =
101 "// Trivial analytic body: a plane at y = 0, solid below. It applies NO\n"
102 "// edits -- exactly like DCTerrainGeneratorProcNodes, and exactly like the\n"
103 "// density stage's DENSITY_GLSL_BODY. That is the contract under test.\n"
104 "float evaluate_density(float x, float y, float z) {\n"
105 " return -y;\n"
106 "}\n";
107
108// --- THE ONE TRANSCRIPTION: apply_gpu_edits' sphere branch -------------------
109// dc_shader_source.h:341-349
110// float dist_sq = edx*edx + edy*edy + edz*edz;
111// if (dist_sq > e.radius * e.radius) continue;
112// float t = 1.0 - dist_sq / (radius*radius);
113// d += e.strength * t * t;
114static float carved(float x, float y, float z) {
115 float d = -y;
116 float ex = x - EDIT_POS[0], ey = y - EDIT_POS[1], ez = z - EDIT_POS[2];
117 float dist_sq = ex * ex + ey * ey + ez * ez;
118 float r_sq = EDIT_RADIUS * EDIT_RADIUS;
119 if (dist_sq <= r_sq) {
120 float t = 1.0f - dist_sq / r_sq;
121 d += EDIT_STRENGTH * t * t;
122 }
123 return d;
124}
125
126static float uncarved(float x, float y, float z) { (void)x; (void)z; return -y; }
127
128// Bisect a field along the test edge. Returns the y of the zero crossing, or
129// NAN when the field does not cross it at all.
130template <typename F>
131static float root_on_edge(F f) {
132 float lo = EDGE_Y0, hi = EDGE_Y0 + VOXEL_SIZE;
133 float dlo = f(EDGE_X, lo, 0.0f), dhi = f(EDGE_X, hi, 0.0f);
134 if ((dlo >= 0.0f) == (dhi >= 0.0f)) return NAN;
135 for (int i = 0; i < 60; i++) {
136 float m = 0.5f * (lo + hi);
137 float dm = f(EDGE_X, m, 0.0f);
138 if ((dlo >= 0.0f) == (dm >= 0.0f)) { lo = m; dlo = dm; } else { hi = m; }
139 }
140 return 0.5f * (lo + hi);
141}
142
143static void carved_normal(float x, float y, float z, float out[3]) {
144 const float h = VOXEL_SIZE * 0.01f;
145 float nx = carved(x + h, y, z) - carved(x - h, y, z);
146 float ny = carved(x, y + h, z) - carved(x, y - h, z);
147 float nz = carved(x, y, z + h) - carved(x, y, z - h);
148 float len = std::sqrt(nx * nx + ny * ny + nz * nz);
149 if (len > 1e-8f) { nx /= len; ny /= len; nz /= len; }
150 out[0] = nx; out[1] = ny; out[2] = nz;
151}
152
153// ---------------------------------------------------------------------------
154// Vulkan scaffolding. Mirrors tests/test_erosion_parity.cpp rather than
155// factoring a shared harness out of it: refactoring the one working GPU
156// instrument in the repo in order to land a test is how you end up with neither.
157// ---------------------------------------------------------------------------
158static void die(const char* what, VkResult r) {
159 fprintf(stderr, "FATAL: %s -> VkResult %d\n", what, (int)r);
160 exit(1);
161}
162#define VK(x) do { VkResult _r = (x); if (_r != VK_SUCCESS) die(#x, _r); } while (0)
163
164static std::string read_file(const char* path) {
165 FILE* f = fopen(path, "rb");
166 if (!f) { fprintf(stderr, "FATAL: cannot open %s\n", path); exit(1); }
167 fseek(f, 0, SEEK_END); long n = ftell(f); fseek(f, 0, SEEK_SET);
168 std::string s(n, '\0');
169 if (fread(&s[0], 1, n, f) != (size_t)n) { fprintf(stderr, "FATAL: short read\n"); exit(1); }
170 fclose(f);
171 return s;
172}
173
174static std::vector<uint32_t> compile_glsl(const std::string& src, const char* tag) {
175 char in_path[] = "/tmp/dc_edit_contract_XXXXXX";
176 int fd = mkstemp(in_path);
177 if (fd < 0) { perror("mkstemp"); exit(1); }
178 FILE* f = fdopen(fd, "w");
179 fwrite(src.data(), 1, src.size(), f);
180 fclose(f);
181 std::string spv_path = std::string(in_path) + ".spv";
182 std::string cmd = "glslc -fshader-stage=comp -O -o " + spv_path + " " + in_path;
183 if (system(cmd.c_str()) != 0) {
184 fprintf(stderr, "FATAL: glslc failed for %s (source left at %s)\n", tag, in_path);
185 exit(1);
186 }
187 FILE* sf = fopen(spv_path.c_str(), "rb");
188 if (!sf) { fprintf(stderr, "FATAL: no SPIR-V at %s\n", spv_path.c_str()); exit(1); }
189 fseek(sf, 0, SEEK_END); long n = ftell(sf); fseek(sf, 0, SEEK_SET);
190 std::vector<uint32_t> spv(n / 4);
191 if (fread(spv.data(), 1, n, sf) != (size_t)n) { fprintf(stderr, "FATAL: short read\n"); exit(1); }
192 fclose(sf);
193 remove(in_path); remove(spv_path.c_str());
194 return spv;
195}
196
197struct Ctx {
198 VkInstance instance = VK_NULL_HANDLE;
199 VkPhysicalDevice phys = VK_NULL_HANDLE;
200 VkDevice dev = VK_NULL_HANDLE;
201 VkQueue queue = VK_NULL_HANDLE;
202 uint32_t qfam = 0;
203 VkCommandPool cmdpool = VK_NULL_HANDLE;
204 VkPhysicalDeviceMemoryProperties memprops{};
205 char devname[256] = {0};
206};
207
208static uint32_t find_mem(Ctx& c, uint32_t bits, VkMemoryPropertyFlags want) {
209 for (uint32_t i = 0; i < c.memprops.memoryTypeCount; i++)
210 if ((bits & (1u << i)) && (c.memprops.memoryTypes[i].propertyFlags & want) == want)
211 return i;
212 fprintf(stderr, "FATAL: no memory type with flags 0x%x\n", want);
213 exit(1);
214}
215
216struct Buf { VkBuffer buf = VK_NULL_HANDLE; VkDeviceMemory mem = VK_NULL_HANDLE;
217 void* mapped = nullptr; VkDeviceSize size = 0; };
218
219static Buf make_host_buf(Ctx& c, VkDeviceSize size, VkBufferUsageFlags usage) {
220 if (size == 0) size = 16;
221 Buf b; b.size = size;
222 VkBufferCreateInfo bi{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
223 bi.size = size; bi.usage = usage; bi.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
224 VK(vkCreateBuffer(c.dev, &bi, nullptr, &b.buf));
225 VkMemoryRequirements mr;
226 vkGetBufferMemoryRequirements(c.dev, b.buf, &mr);
227 VkMemoryAllocateInfo ai{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
228 ai.allocationSize = mr.size;
229 ai.memoryTypeIndex = find_mem(c, mr.memoryTypeBits,
230 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
231 VK(vkAllocateMemory(c.dev, &ai, nullptr, &b.mem));
232 VK(vkBindBufferMemory(c.dev, b.buf, b.mem, 0));
233 VK(vkMapMemory(c.dev, b.mem, 0, VK_WHOLE_SIZE, 0, &b.mapped));
234 memset(b.mapped, 0, (size_t)size);
235 return b;
236}
237
238static void init_vulkan(Ctx& c) {
239 VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO};
240 app.pApplicationName = "dc-terrain edit contract oracle";
241 app.apiVersion = VK_API_VERSION_1_1;
242 VkInstanceCreateInfo ii{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO};
243 ii.pApplicationInfo = &app;
244 VK(vkCreateInstance(&ii, nullptr, &c.instance));
245 uint32_t n = 0;
246 VK(vkEnumeratePhysicalDevices(c.instance, &n, nullptr));
247 if (!n) { fprintf(stderr, "FATAL: no Vulkan device\n"); exit(1); }
248 std::vector<VkPhysicalDevice> devs(n);
249 VK(vkEnumeratePhysicalDevices(c.instance, &n, devs.data()));
250 c.phys = devs[0];
251 for (auto d : devs) {
252 VkPhysicalDeviceProperties p;
253 vkGetPhysicalDeviceProperties(d, &p);
254 if (p.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) { c.phys = d; break; }
255 }
256 VkPhysicalDeviceProperties props;
257 vkGetPhysicalDeviceProperties(c.phys, &props);
258 snprintf(c.devname, sizeof(c.devname), "%s", props.deviceName);
259 vkGetPhysicalDeviceMemoryProperties(c.phys, &c.memprops);
260 uint32_t qn = 0;
261 vkGetPhysicalDeviceQueueFamilyProperties(c.phys, &qn, nullptr);
262 std::vector<VkQueueFamilyProperties> qs(qn);
263 vkGetPhysicalDeviceQueueFamilyProperties(c.phys, &qn, qs.data());
264 bool found = false;
265 for (uint32_t i = 0; i < qn; i++)
266 if (qs[i].queueFlags & VK_QUEUE_COMPUTE_BIT) { c.qfam = i; found = true; break; }
267 if (!found) { fprintf(stderr, "FATAL: no compute queue\n"); exit(1); }
268 float prio = 1.0f;
269 VkDeviceQueueCreateInfo qi{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO};
270 qi.queueFamilyIndex = c.qfam; qi.queueCount = 1; qi.pQueuePriorities = &prio;
271 VkDeviceCreateInfo di{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO};
272 di.queueCreateInfoCount = 1; di.pQueueCreateInfos = &qi;
273 VK(vkCreateDevice(c.phys, &di, nullptr, &c.dev));
274 vkGetDeviceQueue(c.dev, c.qfam, 0, &c.queue);
275 VkCommandPoolCreateInfo ci{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
276 ci.queueFamilyIndex = c.qfam;
277 ci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
278 VK(vkCreateCommandPool(c.dev, &ci, nullptr, &c.cmdpool));
279}
280
281// --- push constants, matching edge_intersect.glsl's block exactly ------------
282struct PC {
283 float cell_origin[3]; float voxel_size;
284 int32_t grid_dims[3]; uint32_t total_elements;
285 uint32_t mesh_quality_flags; uint32_t pad0, pad1, pad2;
286};
287
288// --- GpuEdit, matching the std430 struct in edge_intersect.glsl --------------
289struct GpuEdit {
290 uint32_t edit_type;
291 float pos_x, pos_y, pos_z;
292 float half_x, half_y, half_z;
293 float radius, strength;
294 uint32_t pad0, pad1, pad2;
295};
296
297int main(int argc, char** argv) {
298 const char* shader_path = "shaders/edge_intersect.glsl";
299 for (int i = 1; i < argc; i++)
300 if (!strcmp(argv[i], "--shader") && i + 1 < argc) shader_path = argv[++i];
301
302 // ---- Build the shader: shipping template, trivial body -----------------
303 std::string tmpl = read_file(shader_path);
304 const std::string marker = "// GENERATOR_SDF_BODY";
305 size_t mpos = tmpl.find(marker);
306 if (mpos == std::string::npos) {
307 fprintf(stderr, "FATAL: GENERATOR_SDF_BODY marker not found in %s\n", shader_path);
308 return 1;
309 }
310 tmpl.replace(mpos, marker.length(), TRIVIAL_BODY);
311
312 printf("edit-contract oracle\n");
313 printf(" shader %s (%zu bytes, main() and all declarations verbatim)\n",
314 shader_path, tmpl.size());
315
316 // ---- The analytic answers ---------------------------------------------
317 float y_carved = root_on_edge(carved);
318 float y_uncarved = root_on_edge(uncarved);
319 if (std::isnan(y_carved)) {
320 fprintf(stderr, "FATAL: the carved field does not cross the test edge -- "
321 "the SCENE is wrong, not the shader.\n");
322 return 1;
323 }
324 float n_ref[3];
325 carved_normal(EDGE_X, y_carved, 0.0f, n_ref);
326 printf(" edge (%.2f, %.2f, 0) -> (%.2f, %.2f, 0)\n",
327 EDGE_X, EDGE_Y0, EDGE_X, EDGE_Y0 + VOXEL_SIZE);
328 printf(" carved crossing at y = %+.5f, normal (%.4f, %.4f, %.4f)\n",
329 y_carved, n_ref[0], n_ref[1], n_ref[2]);
330 if (std::isnan(y_uncarved))
331 printf(" uncarved field does NOT cross this edge at all (plane is at y = 0)\n");
332 else
333 printf(" uncarved crossing at y = %+.5f\n", y_uncarved);
334
335 Ctx c{};
336 init_vulkan(c);
337 printf(" device %s\n\n", c.devname);
338
339 std::vector<uint32_t> spv = compile_glsl(tmpl, "edge_intersect + trivial body");
340
341 // ---- Buffers, one per declared binding ---------------------------------
342 const int density_dim = GRID_DIMS + 1; // 33
343 const size_t density_n = (size_t)density_dim * density_dim * density_dim;
344 const size_t edge_n = (size_t)GRID_DIMS * GRID_DIMS * GRID_DIMS * 3;
345
346 const VkBufferUsageFlags SSBO = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
347 const VkBufferUsageFlags UBO = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;
348
349 Buf b_density = make_host_buf(c, density_n * 4, SSBO);
350 Buf b_flags = make_host_buf(c, edge_n * 4, SSBO);
351 Buf b_scan = make_host_buf(c, edge_n * 4, SSBO);
352 Buf b_out = make_host_buf(c, 64 * 4, SSBO);
353 Buf b_params = make_host_buf(c, 256, UBO);
354 Buf b_voronoi = make_host_buf(c, 256, SSBO);
355 Buf b_perm = make_host_buf(c, 3072 * 4, SSBO);
356 Buf b_edits = make_host_buf(c, 16 + sizeof(GpuEdit), SSBO);
357 Buf b_active = make_host_buf(c, 64 * 4, SSBO);
358 Buf b_counts = make_host_buf(c, 64 * 4, SSBO);
359 Buf b_brick = make_host_buf(c, density_n * 4, SSBO);
360 Buf b_planes = make_host_buf(c, 64 * 16, SSBO);
361
362 // The one active edge: voxel (0,0,0), dir 1 (the Y edge).
363 const uint32_t dir = 1u;
364 const uint32_t gid = 0u * 3u + dir;
365 ((uint32_t*)b_active.mapped)[0] = gid;
366 ((uint32_t*)b_counts.mapped)[0] = 1u;
367 ((uint32_t*)b_scan.mapped)[gid] = 0u;
368
369 // Density endpoints = the CARVED field, because that is what the density
370 // stage really produces. The disagreement between the two stages is the
371 // whole subject.
372 const int d_idx0 = 0;
373 const int d_idx1 = d_idx0 + density_dim; // dir == 1 steps by density_dim
374 ((float*)b_density.mapped)[d_idx0] = carved(EDGE_X, EDGE_Y0, 0.0f);
375 ((float*)b_density.mapped)[d_idx1] = carved(EDGE_X, EDGE_Y0 + VOXEL_SIZE, 0.0f);
376
377 // One spherical subtract. flags = 0, so the brick path is off and the
378 // inline/edit path is the one under test.
379 ((uint32_t*)b_edits.mapped)[0] = 1u;
380 GpuEdit* ge = (GpuEdit*)((char*)b_edits.mapped + 16);
381 memset(ge, 0, sizeof(*ge));
382 ge->edit_type = EDIT_TYPE;
383 ge->pos_x = EDIT_POS[0]; ge->pos_y = EDIT_POS[1]; ge->pos_z = EDIT_POS[2];
384 ge->radius = EDIT_RADIUS;
385 ge->strength = EDIT_STRENGTH;
386
387 // ---- Descriptors -------------------------------------------------------
388 const int NB = 12;
389 VkDescriptorSetLayoutBinding binds[NB]{};
390 for (int i = 0; i < NB; i++) {
391 binds[i].binding = i;
392 binds[i].descriptorType = (i == 4) ? VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER
393 : VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
394 binds[i].descriptorCount = 1;
395 binds[i].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
396 }
397 VkDescriptorSetLayoutCreateInfo dli{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
398 dli.bindingCount = NB; dli.pBindings = binds;
399 VkDescriptorSetLayout dsl;
400 VK(vkCreateDescriptorSetLayout(c.dev, &dli, nullptr, &dsl));
401
402 VkPushConstantRange pcr{VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(PC)};
403 VkPipelineLayoutCreateInfo pli{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
404 pli.setLayoutCount = 1; pli.pSetLayouts = &dsl;
405 pli.pushConstantRangeCount = 1; pli.pPushConstantRanges = &pcr;
406 VkPipelineLayout playout;
407 VK(vkCreatePipelineLayout(c.dev, &pli, nullptr, &playout));
408
409 VkShaderModuleCreateInfo smi{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
410 smi.codeSize = spv.size() * 4; smi.pCode = spv.data();
411 VkShaderModule mod;
412 VK(vkCreateShaderModule(c.dev, &smi, nullptr, &mod));
413
414 VkComputePipelineCreateInfo cpi{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO};
415 cpi.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
416 cpi.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT;
417 cpi.stage.module = mod; cpi.stage.pName = "main";
418 cpi.layout = playout;
419 VkPipeline pipe;
420 VK(vkCreateComputePipelines(c.dev, VK_NULL_HANDLE, 1, &cpi, nullptr, &pipe));
421
422 VkDescriptorPoolSize ps[2] = {
423 {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, NB - 1},
424 {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1},
425 };
426 VkDescriptorPoolCreateInfo dpi{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
427 dpi.maxSets = 1; dpi.poolSizeCount = 2; dpi.pPoolSizes = ps;
428 VkDescriptorPool dpool;
429 VK(vkCreateDescriptorPool(c.dev, &dpi, nullptr, &dpool));
430 VkDescriptorSetAllocateInfo dai{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
431 dai.descriptorPool = dpool; dai.descriptorSetCount = 1; dai.pSetLayouts = &dsl;
432 VkDescriptorSet dset;
433 VK(vkAllocateDescriptorSets(c.dev, &dai, &dset));
434
435 Buf* order[NB] = { &b_density, &b_flags, &b_scan, &b_out, &b_params,
436 &b_voronoi, &b_perm, &b_edits, &b_active, &b_counts,
437 &b_brick, &b_planes };
438 VkDescriptorBufferInfo dbi[NB];
439 VkWriteDescriptorSet w[NB]{};
440 for (int i = 0; i < NB; i++) {
441 dbi[i] = { order[i]->buf, 0, VK_WHOLE_SIZE };
442 w[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
443 w[i].dstSet = dset; w[i].dstBinding = i; w[i].descriptorCount = 1;
444 w[i].descriptorType = binds[i].descriptorType;
445 w[i].pBufferInfo = &dbi[i];
446 }
447 vkUpdateDescriptorSets(c.dev, NB, w, 0, nullptr);
448
449 PC pc{};
450 pc.cell_origin[0] = EDGE_X; pc.cell_origin[1] = EDGE_Y0; pc.cell_origin[2] = 0.0f;
451 pc.voxel_size = VOXEL_SIZE;
452 pc.grid_dims[0] = pc.grid_dims[1] = pc.grid_dims[2] = GRID_DIMS;
453 pc.total_elements = 1;
454 pc.mesh_quality_flags = 0;
455
456 VkCommandBufferAllocateInfo cbi{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
457 cbi.commandPool = c.cmdpool; cbi.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
458 cbi.commandBufferCount = 1;
459 VkCommandBuffer cmd;
460 VK(vkAllocateCommandBuffers(c.dev, &cbi, &cmd));
461 VkCommandBufferBeginInfo cbb{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
462 cbb.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
463 VK(vkBeginCommandBuffer(cmd, &cbb));
464 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, pipe);
465 vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, playout, 0, 1, &dset, 0, nullptr);
466 vkCmdPushConstants(cmd, playout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(PC), &pc);
467 vkCmdDispatch(cmd, 1, 1, 1);
468 VK(vkEndCommandBuffer(cmd));
469 VkSubmitInfo si{VK_STRUCTURE_TYPE_SUBMIT_INFO};
470 si.commandBufferCount = 1; si.pCommandBuffers = &cmd;
471 VK(vkQueueSubmit(c.queue, 1, &si, VK_NULL_HANDLE));
472 VK(vkQueueWaitIdle(c.queue));
473
474 const float* got = (const float*)b_out.mapped;
475 float px = got[0], py = got[1], pz = got[2];
476 float nx = got[3], ny = got[4], nz = got[5];
477
478 float dy = py - y_carved;
479 float dot = nx * n_ref[0] + ny * n_ref[1] + nz * n_ref[2];
480 if (dot > 1.0f) dot = 1.0f;
481 if (dot < -1.0f) dot = -1.0f;
482 float nerr = std::acos(dot) * 57.2957795f;
483
484 printf("mesh stage returned\n");
485 printf(" position (%+.5f, %+.5f, %+.5f)\n", px, py, pz);
486 printf(" normal (%.4f, %.4f, %.4f)\n", nx, ny, nz);
487 printf("\n");
488 printf(" crossing error vs carved surface %+.5f m\n", dy);
489 printf(" normal error vs carved gradient %6.2f deg\n", nerr);
490 printf("\n");
491
492 bool ok = (std::fabs(dy) < 0.01f) && (nerr < 3.0f);
493 if (ok) {
494 printf("PASS the mesh stage samples the carved field for a body that "
495 "does not apply edits\n");
496 return 0;
497 }
498 printf("FAIL the mesh stage is NOT sampling the carved field.\n");
499 printf(" evaluate_density() returns the RAW field for this body, and\n");
500 printf(" edge_intersect.glsl applies no edits of its own, so the\n");
501 printf(" crossing search and the normals ride an uncarved surface\n");
502 printf(" while the density endpoints are carved. This is what breaks\n");
503 printf(" CSG on every custom generator (map_dc_erosion).\n");
504 return 1;
505}