From b95c6579bdccb8b896d8702af297d4c42b7ee1df Mon Sep 17 00:00:00 2001 From: Yingjie Wang Date: Sat, 29 Aug 2026 01:17:47 -0400 Subject: [PATCH] Frame: gate fold refinement by Jacobian --- README.md | 15 +++++++------ src/frame.c | 49 +++++++++++++++++++++++++++++++---------- tests/test_frame.c | 30 ++++++++++++++++--------- video_rendering_plan.md | 2 +- 4 files changed, 65 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index a7564b1..f2faa46 100644 --- a/README.md +++ b/README.md @@ -180,13 +180,14 @@ computes the discrete lens Jacobian `J = Omega_source / Omega_image`. Both signed solid angles use `2 atan2(dot(a, cross(b,c)), 1 + dot(a,b) + dot(b,c) + dot(c,a))`, with the ordered camera directions for `Omega_image` and their traced infinity -directions for `Omega_source`. `abs(J) < --refine-jacobian-min` requests a -split near a critical curve. The signed-area calculation retains the sign of -`J`, but sign changes across adjacent triangles are not currently a refinement -trigger; this makes it possible to evaluate the `abs(J)` criterion separately. -The `1e-3` default is deliberately provisional and should be tuned with the -small Schwarzschild refinement diagnostic before being treated as a production -threshold. +directions for `Omega_source`. A J-driven split requires **both** a shared +image edge whose incident triangles have opposite nonzero signs of `J` and +`min(abs(J_left), abs(J_right)) < --refine-jacobian-min`. It then requests +that shared edge on both leaves. Thus `|J|` bounds the fold selection instead +of widening it as a standalone critical-curve band. A negative sign is +physical parity and is retained. The `1e-3` default is deliberately +provisional and should be tuned with the small Schwarzschild refinement +diagnostic before being treated as a production threshold. For a short Schwarzschild diagnostic that permits at most one actual split generation, for example: diff --git a/src/frame.c b/src/frame.c index 904ea4e..0c4a531 100644 --- a/src/frame.c +++ b/src/frame.c @@ -344,9 +344,11 @@ static double spherical_signed_area(const double a[3], const double b[3], } /* Returns whether the discrete source/image solid-angle ratio is available. - * Captured and degenerate image triangles have no reliable value. */ + * A zero output parity is a valid, critical (zero-Jacobian) result; captured + * and degenerate image triangles have no reliable parity. */ static int discrete_jacobian(const FrameLensMesh *mesh, - const LensTriangle *triangle, double *value) { + const LensTriangle *triangle, double *value, + signed char *parity) { const LensVertex *a = &mesh->vertices[triangle->vertex[0]]; const LensVertex *b = &mesh->vertices[triangle->vertex[1]]; const LensVertex *c = &mesh->vertices[triangle->vertex[2]]; @@ -363,6 +365,7 @@ static int discrete_jacobian(const FrameLensMesh *mesh, if (!isfinite(jacobian)) return 0; *value = jacobian; + *parity = jacobian > 0.0 ? 1 : jacobian < 0.0 ? -1 : 0; return 1; } @@ -456,8 +459,11 @@ int frame_lens_mesh_finish_generation(FrameLensMesh *mesh, MeshEdge *edges = calloc(edge_count, sizeof *edges); unsigned char *requested = calloc(edge_count, sizeof *requested); unsigned char *allowed = calloc(mesh->triangle_count, sizeof *allowed); - if (edges == NULL || requested == NULL || allowed == NULL) { - free(edges); free(requested); free(allowed); + signed char *parity = calloc(mesh->triangle_count, sizeof *parity); + double *jacobians = calloc(mesh->triangle_count, sizeof *jacobians); + if (edges == NULL || requested == NULL || allowed == NULL || parity == NULL || + jacobians == NULL) { + free(edges); free(requested); free(allowed); free(parity); free(jacobians); return -1; } for (size_t i = 0; i < mesh->triangle_count; ++i) { @@ -472,13 +478,32 @@ int frame_lens_mesh_finish_generation(FrameLensMesh *mesh, if (allowed[i]) { const unsigned int side = longest_side(mesh, triangle); requested[3 * i + side] = probe_requires_split(mesh, triangle, side, config); - double jacobian; - if (discrete_jacobian(mesh, triangle, &jacobian) && - fabs(jacobian) < config->jacobian_minimum) - requested[3 * i + side] = 1; + (void)discrete_jacobian(mesh, triangle, &jacobians[i], &parity[i]); } } qsort(edges, edge_count, sizeof *edges, compare_mesh_edge); + /* A fold is selected only when its adjacent discrete parities disagree and + * at least one of those leaves is close enough to the critical curve. */ + for (size_t first = 0; first < edge_count;) { + size_t last = first + 1; + while (last < edge_count && edges[last].a == edges[first].a && + edges[last].b == edges[first].b) + ++last; + if (last - first == 2) { + const size_t left = edges[first].triangle; + const size_t right = edges[first + 1].triangle; + if (parity[left] != 0 && parity[right] != 0 && + parity[left] != parity[right] && + fmin(fabs(jacobians[left]), fabs(jacobians[right])) < + config->jacobian_minimum) { + if (allowed[left] && allowed[right]) { + requested[3 * left + edges[first].side] = 1; + requested[3 * right + edges[first + 1].side] = 1; + } + } + } + first = last; + } /* A requested interior edge is split by both incident leaves, preserving a * conforming mesh. If either side has reached its geometric limit, reject * the whole edge instead of introducing a T-junction. */ @@ -542,7 +567,7 @@ int frame_lens_mesh_finish_generation(FrameLensMesh *mesh, if (split_edges == 0) { mesh->sample_count = 0; mesh->samples_include_probes = 0; - free(edges); free(requested); free(allowed); + free(edges); free(requested); free(allowed); free(parity); free(jacobians); return 0; } /* Allocate a single stable midpoint vertex for each requested edge group. */ @@ -559,7 +584,7 @@ int frame_lens_mesh_finish_generation(FrameLensMesh *mesh, first = last; } if (ensure_vertices(mesh, mesh->vertex_count + midpoint_count)) { - free(edges); free(requested); free(allowed); + free(edges); free(requested); free(allowed); free(parity); free(jacobians); return -1; } size_t next_vertex = mesh->vertex_count; @@ -586,7 +611,7 @@ int frame_lens_mesh_finish_generation(FrameLensMesh *mesh, const size_t old_count = mesh->triangle_count; LensTriangle *children = calloc(old_count * 4, sizeof *children); if (children == NULL) { - free(edges); free(requested); free(allowed); + free(edges); free(requested); free(allowed); free(parity); free(jacobians); return -1; } size_t child_count = 0; @@ -634,7 +659,7 @@ int frame_lens_mesh_finish_generation(FrameLensMesh *mesh, mesh->vertex_count = next_vertex; mesh->sample_count = 0; mesh->samples_include_probes = 0; - free(edges); free(requested); free(allowed); + free(edges); free(requested); free(allowed); free(parity); free(jacobians); return (int)midpoint_count; } diff --git a/tests/test_frame.c b/tests/test_frame.c index 46e986a..537746f 100644 --- a/tests/test_frame.c +++ b/tests/test_frame.c @@ -193,24 +193,32 @@ int main(void) { goto done; } frame_lens_mesh_destroy(&adaptive_mesh); - /* A zero discrete Jacobian is an independent fold/critical-band trigger: - * the probe below agrees exactly with linear endpoint interpolation, so the - * direction-error criterion cannot request this split. */ - if (frame_lens_mesh_build_coarse(&adaptive_mesh, width, height, 100, 30.0)) - goto done; - for (size_t i = 0; i < adaptive_mesh.vertex_count; ++i) { - adaptive_mesh.vertices[i].traced = 1; - adaptive_mesh.vertices[i].status = RAY_ENDPOINT_ESCAPED; - adaptive_mesh.vertices[i].n_infinity[0] = 1.0; - } const RayEndpoint flat_probe = {.n_infinity = {1.0, 0.0, 0.0}, .frequency_ratio = 1.0, .status = RAY_ENDPOINT_ESCAPED}; + /* Opposite nonzero discrete-Jacobian signs on the two sides of the shared + * diagonal require a sufficiently small magnitude before requesting it. */ + refine.jacobian_minimum = 10.0; + refine.angle_absolute_rad = 3.14159265358979323846; + refine.angle_relative = 1e6; + if (frame_lens_mesh_build_coarse(&adaptive_mesh, width, height, 100, 30.0)) + goto done; + const double source_directions[4][3] = { + {1.0, 0.0, 0.0}, + {sqrt(0.99), 0.0, 0.1}, + {sqrt(0.99), 0.0, 0.1}, + {sqrt(0.98), 0.1, 0.1}}; + for (size_t i = 0; i < adaptive_mesh.vertex_count; ++i) { + adaptive_mesh.vertices[i].traced = 1; + adaptive_mesh.vertices[i].status = RAY_ENDPOINT_ESCAPED; + memcpy(adaptive_mesh.vertices[i].n_infinity, source_directions[i], + sizeof source_directions[i]); + } if (frame_lens_mesh_prepare_generation(&adaptive_mesh, &refine) != 1 || frame_lens_mesh_install_sample(&adaptive_mesh, 0, &flat_probe) || frame_lens_mesh_finish_generation(&adaptive_mesh, &refine) != 1 || adaptive_mesh.vertex_count != 5 || adaptive_mesh.triangle_count != 4) { - fputs("adaptive discrete-Jacobian split regression failed\n", stderr); + fputs("adaptive fold-parity split regression failed\n", stderr); frame_lens_mesh_destroy(&adaptive_mesh); goto done; } diff --git a/video_rendering_plan.md b/video_rendering_plan.md index e02ed46..91958ce 100644 --- a/video_rendering_plan.md +++ b/video_rendering_plan.md @@ -88,7 +88,7 @@ SampleRequest -> RayPool (SoA, inactive / active / terminated) ## Phase D:补全多 pass adaptive mesh 与 out-of-core 准备(未开始) -1. `FrameLensMesh` 采用 append-only stable vertex ID、generation-local request/probe 队列、triangle level 与“缺失 endpoint”状态。每 pass 的 probe 只放在最长 image-plane 边中点;完整 sweep 后才安装 endpoint 并改变拓扑。细分保留同时超过绝对与相对 `n_infinity` 方向误差阈值的几何判据,并受最大层数、最小长边和最小面积约束;capture/escape 不一致强制细分。另以三顶点的有向球面面积比 `J = Omega_source / Omega_image` 独立检测临界带:当前仅 `abs(J)` 小于阈值时细分;相邻三角形的 `J` 异号细分暂不启用,留待验证 `abs(J)` 的覆盖能力后再决定。 +1. `FrameLensMesh` 采用 append-only stable vertex ID、generation-local request/probe 队列、triangle level 与“缺失 endpoint”状态。每 pass 的 probe 只放在最长 image-plane 边中点;完整 sweep 后才安装 endpoint 并改变拓扑。细分保留同时超过绝对与相对 `n_infinity` 方向误差阈值的几何判据,并受最大层数、最小长边和最小面积约束;capture/escape 不一致强制细分。另以三顶点的有向球面面积比 `J = Omega_source / Omega_image` 检测 fold:仅当共享边两侧非零 `J` 异号,且 `min(abs(J_left), abs(J_right))` 小于阈值时,细分该共享边的两叶;负 parity 本身保留而不剔除。 2. 以迭代 queue(可按 frame/root tile 并行、线程本地 request buffer 后 sort/deduplicate)替代递归 task。每一个 pass 完整执行 Phase C sweep;只有所有 frame 都无新请求才进行 catalog splat。 3. 在 movie 生命周期中及时释放已完成的 RayPool、临时 request 和单帧 HDR;保留最终 mesh/endpoints,或在渲染 PNG 后按明确策略释放,避免视频时无界增长。 4. 为 nmesh 预留并实现 source-side slab overlap / temporal ghost-slice 契约、可配置 memory budget、slab coverage 日志和线程本地 `MetricWorkspace` ownership。此 phase 不重采样为 Cartesian grid,也不假定相邻时间 slice 的 AMR tree 相同。