Compare commits

..
8 Commits
Author SHA1 Message Date
Clifford Yapp aa96b975c9 Support using OpenQMC for Cauchy/Crofton sampling instead of PRNG 2026-09-18 09:59:52 -04:00
Clifford Yapp 5434e46ff0 facetize: add source-guided adaptive SPSR
Fixed-size point dumps make Poisson reconstruction unreliable as a
last-resort facetization method.  They can miss thin partitions while
oversampling surfaces that are already well resolved, and an arbitrary
point count does not show whether source characterization is complete.

Seed reconstruction with stabilized Crofton samples.  Retain paired
entry and exit normals, partition thickness, and ray identities so thin
geometry contributes explicit local information instead of unrelated
points.

Add a bounded adaptive libbg driver around the existing upstream
Poisson solver.  Each solve reports residual, density, normal, and
surface-variation hints to a caller callback.  Facetize responds with
exact source rays and validates partitions, endpoints, chord length,
area, volume, and closed-manifold topology before accepting a result.

Bound refinement by time, point, pass, and convergence limits.  Relax
only chord and thin-partition thresholds when the source BoT is already
known to be non-manifold.  Deprecate the fixed-array API, remove the
intermediate point-sampling path, adapt pnts callers, and add focused
coverage.  The vendored SPSR implementation remains unchanged.

Use configured method priority for perturbation variants so an MDC-only
run never invokes NMG.  Report finalization progress and retry counts
while the fallback hierarchy is evaluated.
2026-09-18 09:59:52 -04:00
Clifford Yapp 2c1dd12033 libanalyze: replace marching cubes with adaptive MDC
The legacy marching-cubes polygonizer sampled a uniform grid and could
miss thin or high-aspect features, merge undersampled tunnels, and emit
surfaces whose shared-edge winding was inconsistent.  Increasing the
global resolution made those failures prohibitively expensive rather
than addressing their local causes.

Add a ray-driven manifold dual contouring API built on an adaptive
octree.  Cache exact signs and Hermite edge crossings from librt
partitions, place feature-preserving vertices with bounded QEF solves,
and refine locally for thin walls, multiple crossings, interior
surfaces, and ambiguous topology.  Build faces from shared crossings
with deterministic winding and require closed manifold output.

Back analyze_polygonize with the new implementation, expose MDC as a
facetize method, and remove the obsolete polygonizer code, licensing,
continuation sampler, and CM default.  Keep NMG available when polygon
preservation is more important and it can successfully process the
input.

Document the Ju, Schaefer, and Warren algorithmic basis and the
bottom-up Euler-characteristic escalation path.  Add coverage for thin
walls, tunnels, overlapping CSG, toroidal cases, winding, resource
limits, and the compatibility polygonalize entry point.
2026-09-18 09:59:52 -04:00
Clifford Yapp ac31dd518e facetize: retire the legacy pipeline and harden region results
The compatibility implementation duplicated command behavior and
retained unreliable paths that had been superseded by the planned
worker pipeline.  Keeping both implementations made option handling and
failure behavior needlessly difficult to reason about.

Remove the old facetize sources and MGED registration so all command
modes use the maintained orchestrator.  Preserve implicitly selected
region roots in the output database and keep region retries scoped to
validated perturbation inputs.

Tighten source and result validation, reject empty preparation results,
and suppress expected import or preparation diagnostics only inside a
scoped log hook.  Keep routine details out of the terminal summary and
write complete detail logs when concise reporting is not possible.

Extend regression and validation coverage for implicit roots, tolerated
failures, and malformed or unsupported inputs.
2026-09-18 09:59:52 -04:00
Clifford Yapp 45e477240a facetize: separate orchestration and isolate NMG evaluation
The command implementation mixed option handling, mode selection,
database setup, scheduling, and result publication.  NMG Boolean work
also ran in the parent process, allowing known NMG failures to terminate
the overall conversion.

Introduce an explicit planning and orchestration layer so each command
mode follows one visible execution path.  Replace positional worker
arguments with typed request and result messages that are validated
before they can alter the working database.

Move NMG Boolean evaluation into the supervised subprocess framework.
Transfer trees and BoTs through the existing transaction protocol,
report failures without killing the parent, and reject primitives that
do not provide the required tessellation callback.

Update facetize regression and worker-protocol coverage for the new
boundaries and failure semantics.
2026-09-18 09:59:52 -04:00
Clifford Yapp b3ad69b490 facetize: add supervised parallel primitive workers
Primitive tessellation previously paid process and database setup
costs per object and left the parent with limited visibility into work
that timed out or died.  Large models consequently spent substantial
time outside the tessellation algorithms themselves.

Add long-lived supervised workers and a bounded scheduler for
independent objects.  Divide CPUs between worker processes and each
worker internal parallelism, reuse opened databases, and retain cache
state across requests.

Make result publication transactional.  A WRITE_STARTED acknowledgment
closes the READY and PROCEED race, while the parent detects timeouts and
abnormal exits, restarts workers, and records only completed object
results.  Reuse temporary result databases without leaking state between
requests.

Add process and database-transfer helpers together with worker,
recovery, and validation tests.  Document the parallel job control and
its resource behavior.
2026-09-18 09:59:52 -04:00
Clifford Yapp 2ad973e7d3 librt: make Crofton sampling deterministic and reusable
Retain ordinary Monte Carlo chord sampling while making its stream
explicit and portable. Draw independent sphere endpoints from a
fixed-seed mt19937_64 generator so every setup is reproducible
without changing the estimator's statistical convergence model.

Convert the generator's high 53 bits explicitly to avoid library-
specific real-number conversions. Consume four values per ray so
ray offsets reproduce disjoint continuations of the same stream.

Reuse a bounded C++ worker pool and stop only after windowed area
estimates remain stable. This avoids repeated thread construction
while retaining the existing stopping criteria.

Add optional collection of ray identifiers, paired entry and exit
hit points, normals, and partition thickness. This gives
reconstruction callers structured source evidence while preserving
lightweight area and volume estimation. Link librt to the thread
target and extend the Crofton tests to verify that offset calls
exactly reproduce the uninterrupted stream.
2026-09-18 09:59:52 -04:00
Clifford Yapp 572cc3afa2 libbu: support resource-aware worker processes
Large subprocess pools need to divide machine resources without allowing
each worker internal parallelism to consume every CPU.  Add a
process-local limit to bu_avail_cpus() and bu_parallel(), with atomic
updates and coverage for applying and removing the limit.

Make BU_MEM_AVAIL count FreeBSD reclaimable inactive and cache queues
in addition to immediately free pages.  This prevents idle systems from
appearing memory-starved to resource-aware algorithms and adds native
sysctl checks for the reported values.

Rename the private process.h to process_private.h.  This prevents the
source header from shadowing the MSVC CRT header used by std::thread,
which otherwise leaves _beginthreadex undeclared.
2026-09-18 09:59:52 -04:00
83 changed files with 12567 additions and 11327 deletions
+3 -2
View File
@@ -18,7 +18,8 @@ Link with *-lanalyze*.
The public interface is organized across the headers included by
_analyze.h_, including _analyze/diff.h_, _analyze/density.h_,
_analyze/grid.h_, _analyze/heal.h_, _analyze/info.h_, _analyze/pnts.h_,
_analyze/polygonize.h_, _analyze/nirt.h_, _analyze/voxelize.h_, and
_analyze/contour.h_, _analyze/polygonize.h_, _analyze/nirt.h_,
_analyze/voxelize.h_, and
_analyze/worker.h_.
== DESCRIPTION
@@ -32,7 +33,7 @@ to gather the information they need.
Facilities provided by the library include model differencing, material
density and mass property analysis, ray-based gridding and sampling,
geometry healing, model information queries, point-cloud generation and
processing, polygonization and voxelization of geometry, and the
processing, manifold dual contouring and voxelization of geometry, and the
interactive ray-tracing engine used by the *nirt*(1) command. A parallel
worker interface supports distributing analysis work across multiple
threads.
+24 -16
View File
@@ -10,7 +10,7 @@ facetize - Creates __new_object__ using __old_object__ as a guide, creating an e
[source]
----
facetize [-h] [-v] [-q] [-n] [-r] [-s suffix] [-p prefix] [--in-place] [--max-time #] [--max-pnts #] [--resume] [--methods m1,m2,...] [--method-opts METHOD opt1=val opt2=val...] [--no-empty] [--log-file filename] [--nmg-booleval] [--disable-fixup] [--perturb] [--no-perturb] [--perturb-sa-tol #] [--perturb-vol-tol #] [--tolerate-failures] [-B -t threshold] {old_object new_object}
facetize [-h] [-v] [-q] [-n] [-r] [-j jobs] [-s suffix] [-p prefix] [--in-place] [--max-time #] [--max-pnts #] [--resume] [--methods m1,m2,...] [--method-opts METHOD opt1=val opt2=val...] [--no-empty] [--log-file filename] [--nmg-booleval] [--disable-fixup] [--perturb] [--no-perturb] [--perturb-sa-tol #] [--perturb-vol-tol #] [--tolerate-failures] [-B -t threshold] {old_object new_object}
----
@@ -45,6 +45,8 @@ By default, the Manifold BoT path can make use of a coplanarity-avoidance pertur
When the perturbation step is active, per-region *facetize* validates the initial Boolean output against the original CSG geometry using a Crofton stochastic raytrace. The default tolerance thresholds that trigger a perturb retry are 10% for both surface area and volume; these can be adjusted with *--perturb-sa-tol* and *--perturb-vol-tol*. At the end of a per-region facetization run *facetize* prints a concise validation summary listing how many regions passed, triggered a perturb retry, or encountered one of the following noteworthy conditions:
_Unbounded halfspace (validation unavailable)_: Crofton surface-area and volume comparisons require a finite sampling domain. A standalone halfspace, or another CSG result without finite bounds, is therefore not sampled and is counted as having validation unavailable. A bounded object intersected with or subtracted by a halfspace can still be validated because its finite operand supplies the sampling bounds.
_Sub-mm / few-hit bypass (accepted with note)_: When the Crofton sampler finds only a very small number of ray-surface intersections (below an internal threshold of 50 crossings), the sampling noise alone would exceed 14% relative error, making any SA comparison meaningless. Geometry in this category is typically sub-millimetre in cross-section (e.g., a TGC with a radius under 0.2 mm) but the stochastic sampler has confirmed that some geometry is present. The BoT conversion is accepted and the region is counted separately in the summary. Users should verify that such extreme geometry reflects actual modeling intent rather than an accidental near-degenerate primitive.
_Zero-hit replacement_: When the Crofton sampler fires rays but records zero intersections with the CSG geometry while the corresponding BoT is non-empty, *facetize* concludes that the Boolean evaluation most likely produces empty geometry (for example, a subtractor that fully engulfs the base object). In this case the non-empty BoT is discarded and replaced with an empty BoT to match the expected raytrace behavior. If this replacement appears unexpected for a given region, the *lint* command can be used to inspect the result.
@@ -61,13 +63,21 @@ Different tolerance settings will create finer or coarser meshes. See the *tol*
Primitive tessellation is performed in a child process against a private working copy of the database in the BRL-CAD cache. The original database is not modified as individual primitive meshes are generated. Only after the conversion succeeds are the requested results transferred from the working database. This separation also permits an interrupted conversion to retain its intermediate state for use with *--resume*.
The parent command sends primitive names to a persistent tessellation process one at a time. The child reports a result for each name, so a timeout, conversion failure, or child-process crash that occurs before a database write can be associated directly with the affected primitive. The child is restarted when necessary and processing continues with the next name; the parent does not subdivide and retry the whole group to discover which object failed. Unless output is suppressed with *--quiet*, long tessellation batches also report their current object and completed-object count at approximately five-second intervals.
The parent command sends primitive names to a small pool of persistent tessellation processes. Each child reports a result for every name, so a timeout, conversion failure, or child-process crash can be associated directly with the affected primitive. A child is restarted when necessary and processing continues with the next name; the parent does not subdivide and retry the whole group to discover which object failed. Unless output is suppressed with *--quiet*, long tessellation batches also report their completed-object count and active-worker state at approximately five-second intervals.
Database updates use a separate write handshake. After preparing a mesh in memory, the child announces that it is ready to write and reports the approximate mesh size, then waits. The parent acknowledges that it is waiting for the write before the child may update the working database. The child explicitly reports when the update is complete. Consequently, the normal per-object tessellation limit, including *--max-time* and method-specific limits, applies to mesh preparation and does not interrupt an acknowledged database write.
By default, *facetize* uses at most two worker processes and only enables the second when both CPU and memory information indicate it is safe. It reserves the larger of one GiB or one quarter of total physical memory for the rest of the system. Starting another job also requires at least 512 MiB beyond that reserve, or the largest worker resident-memory measurement seen so far if that is greater. If the platform cannot report the required memory information, *facetize* falls back to one worker. The available CPU count is divided among worker processes and is also imposed as the thread limit inside each worker, preventing nested algorithms from independently using all CPUs. *--jobs* changes the maximum process count; it does not bypass the memory admission checks.
Per-region BoT conversion uses this worker policy for both independent CSG validation and initial Manifold Boolean evaluation. When more than one worker is available, persistent validation processes each open their own read-only handle to the original database and compute Crofton surface-area and volume references for different regions. Persistent Boolean processes then evaluate different region trees using independent read-only handles to one shared snapshot of the tessellated working database. That snapshot also serves as the rollback checkpoint, so increasing *--jobs* does not create additional full database copies. This process isolation avoids sharing mutable raytrace preparation or database state between concurrent evaluations. Each result remains associated with the requested region name. The parent commits completed BoTs through one writer and continues to perform hierarchy changes, validation decisions, and any perturb retries serially. A one-worker run retains the established in-process order and avoids the snapshot phase.
Validation and Boolean workers report their completed-region count and active-worker state approximately every five seconds. When *--max-time* is specified, it also bounds each parallel CSG reference or Boolean request; a timed-out process is terminated, and the failure is associated with that region. Failure or timeout of supplementary CSG validation does not cause unrelated regions to be retried.
Database updates use a separate write handshake. After preparing a mesh in memory, a child announces that it is ready to write and reports the approximate mesh size and its current resident memory, then waits. The parent grants write permission, the child acknowledges when it is starting the write, and the child explicitly reports when the update is complete. Consequently, the normal per-object tessellation limit, including *--max-time* and method-specific limits, applies to mesh preparation and does not interrupt an acknowledged write.
With multiple workers, primitive tessellation children write to small staging databases rather than modifying the shared working database. Region Boolean children read one immutable cache-local snapshot while completed results are committed to the working database. The snapshot doubles as the recovery checkpoint and is opened independently by each worker; it is not duplicated per worker. For very large files, snapshot preparation reports the amount copied and elapsed time at five-second intervals. A region result is first completed in memory and enters the write handshake only when it is ready to replace the prior staged result. After a child reports a completed staging write, the parent submits that result to a single persistent writer process. The writer announces that it is ready, waits for the parent's permission, acknowledges that it is starting the write, transfers the result through one persistent working-database handle, and reports completion. This permits tessellation and initial region Boolean evaluation to proceed in parallel without concurrent database allocation updates or repeated full directory scans, while allowing the parent to supervise the canonical database write itself. With one worker, the established direct path avoids the extra staging I/O and database snapshot.
Before the first primitive conversion, the parent measures write throughput on the same cache filesystem as the working database. It writes a temporary file in 1 MiB chunks until approximately one second has elapsed or 128 MiB has been written, then removes the file immediately. Successful mesh writes of at least 1 MiB subsequently refine this profile. For each database update, the parent allows ten times the projected write time, with a minimum of five seconds and a maximum of five minutes. This measured allowance protects legitimate long writes on slow storage while still detecting a genuinely stalled update.
If a child fails before write permission is granted, the working database is known to be unchanged and no rollback is needed. A failure, crash, or write-deadline expiration after permission is granted but before completion is ambiguous: the working database may contain a partial update. In that case *facetize* restores its cache-local checkpoint and replays only the primitive conversions already known to have succeeded. The original database remains untouched during this recovery.
If a child fails before acknowledging that its write has started, the working database is known to be unchanged and no rollback is needed. In parallel mode, a failure during a child write affects only that worker's disposable staging database, so the failed object is reported and the worker can be restarted without replaying unrelated objects. A serial child failure during a direct write, or a writer-process failure after acknowledging write start, is ambiguous because the working database may contain a partial update. In those cases *facetize* restores its cache-local checkpoint and replays only the conversions not already tied to a specific failure. The original database remains untouched during this recovery.
[[facetize_options]]
@@ -94,6 +104,10 @@ Instructs *facetize* to produce NMG solids rather than BoT solids.
Rather than converting all specified inputs to one top level object, walk specified trees and individually convert regions to facetized objects. This mode is useful when wanting to display models in shaded mode while preserving color and region ID properties. If no regions are present a normal conversion is performed instead.
*-j, --jobs #*::
Set the maximum number of facetize worker processes. A value of 1 forces serial processing. The default value of 0 selects a conservative automatic limit based on available CPUs and memory. Memory checks may delay or reduce parallel work even when a larger value is requested. In per-region BoT mode the same limit controls parallel CSG reference sampling and initial Manifold Boolean evaluation.
*-s, --suffix*::
User specified suffix to use avoiding naming conflicts (naming conflict avoidance is needed when creating objects in *-r* mode.)
@@ -119,7 +133,7 @@ Resume an incomplete facetization process (will utilize cached data from a previ
*--methods*::
Accepts a comma separated list of one or more facetization methods to use. The default is to try all algorithms available as needed, but if a user knows they want to try a specific methodology they can call it out using this option.
Accepts a comma separated list of one or more facetization methods to use. The default tries NMG tessellation first, Manifold Dual Contouring (MDC) second, and SPSR as a final fallback. Other listed methods may be selected explicitly if a user wants to try a specific methodology.
*--method-opts*::
@@ -231,21 +245,15 @@ mged>**facetize --tolerate-failures large_model large_model.partial.bot**
====
.Increase Decimation in CM Mode
.Refine a Manifold Dual Contouring conversion
[example]
====
mged>**facetize --methods CM --method-opts feature_size=0.06 d_feature_size=2.0 ellipse.r ell.bot**
This selects MDC directly and requests a finer cell size with a larger ray
budget than the default:
The default settings on this object in CM mode will produce a little over 8000 faces in the final bot, but the addition of the *d_feature_size* option with a >1.5 value has dramatically reduced the count to 104.
mged>**facetize --methods MDC --method-opts "MDC feature_size=0.1 max_rays=400000" ellipse.r ell.bot**
====
.Decrease Decimation in CM Mode
[example]
====
mged>**facetize --methods CM --method-opts feature_size=0.06 d_feature_size=0.01 ellipse.r ell.bot**
If the intent is preserve a more detailed mesh, reducing *decimation-feature-size* will keep more of the original triangles - in the above example, almost 250 thousand of the original 6.9 million faces were retained. NOTE: use lower values here cautiously, as extremely large meshes can be very difficult for many tools to deal with.
Smaller feature sizes increase mesh detail, ray count, memory use, and runtime.
====
+13 -16
View File
@@ -679,23 +679,21 @@ Convert CSG geometry to a 3D voxel grid by:
fill fraction
* Level-of-detail parameter controls sub-voxel sampling
==== 7.5 Implicit Surface Polygonizer (Marching Cubes / Tetrahedral)
==== 7.5 Manifold Dual Contouring
An implicit surface polygonizer (Bloomenthal tetrahedral marching) for
converting any inside/outside function to a triangle mesh.
`contour.cpp` evaluates CSG geometry directly with `librt` and constructs
closed, oriented triangle meshes:
`polygonizer.c` (based on Jules Bloomenthal’s "`An Implicit Surface
Polygonizer,`" Graphics Gems IV, 1994) converts any implicit function
(inside/outside test) to a triangle mesh:
* Maintains a hash table of evaluated lattice corners and edges
* Expanding front: starts at a seed point and spreads outward, testing
adjacent cube faces
* Each transitive cube is optionally decomposed into 6 tetrahedra for
sub-cube accuracy
* Edge-surface crossing is found by bisection (`converge`) then the
vertex normal is estimated from finite differences
* Used in `libanalyze` for converting CSG regions to faceted form
* Recursively refines an implicit octree using cached axis-aligned rays along
node box edges and interior quarter- and center-lines
* Reuses complete ray partitions at the conforming finest grid and retains
evaluated boundary points with analytic surface normals
* Resolves ambiguous faces consistently and permits multiple vertices in a
cell to preserve local topology
* Places vertices with bounded quadratic-error minimization
* Validates the resulting indexed mesh before returning it
* Backs the legacy `analyze_polygonize` interface without retaining the old
seed-driven marching-tetrahedra implementation
==== 7.6 Geometry Differencing (raydiff)
@@ -1712,7 +1710,6 @@ import
runtime
* *MT19937*: Mersenne Twister PRNG
* *pbrt-v3* (derived): HLBVH construction algorithm
* *Bloomenthal polygonizer*: implicit surface polygonization
* *Screened Poisson Reconstruction* (Michael Kazhdan): surface
reconstruction from point clouds
* *Ball Pivoting Algorithm*: surface reconstruction
-1
View File
@@ -55,7 +55,6 @@ brlcad_manage_files(pbrt.txt ${DOC_DIR}/legal/embedded TRIGGER
brlcad_manage_files(picohash.txt ${DOC_DIR}/legal/embedded TRIGGER gist)
brlcad_manage_files(pixcii.txt ${DOC_DIR}/legal/embedded TRIGGER libicv)
brlcad_manage_files(point_in_polygon.txt ${DOC_DIR}/legal/embedded TRIGGER libbg)
brlcad_manage_files(polygonizer.txt ${DOC_DIR}/legal/embedded TRIGGER libanalyze)
brlcad_manage_files(predicates.txt ${DOC_DIR}/legal/embedded TRIGGER libbg)
brlcad_manage_files(profont.txt ${DOC_DIR}/legal/embedded TRIGGER libbu)
brlcad_manage_files(pstdint.txt ${DOC_DIR}/legal/embedded TRIGGER libbu)
-9
View File
@@ -1,9 +0,0 @@
https://www.bloomenthal.com/JBloom/code/polygonizer.c
Authored by Jules Bloomenthal, Xerox PARC.
Copyright (c) Xerox Corporation, 1991. All rights reserved.
Permission is granted to reproduce, use and distribute this code for
any and all purposes, provided that this notice appears in all copies.
file:/src/libanalyze/polygonizer.c
+1
View File
@@ -31,6 +31,7 @@
#include "common.h"
#include "analyze/defines.h"
#include "analyze/contour.h"
#include "analyze/debug.h"
#include "analyze/diff.h"
#include "analyze/density.h"
+1
View File
@@ -1,5 +1,6 @@
set(
analyze_headers
contour.h
debug.h
defines.h
density.h
+118
View File
@@ -0,0 +1,118 @@
/* C O N T O U R . H
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this file; see the file named COPYING for more
* information.
*/
/** @addtogroup libanalyze
* @{ */
/** @file analyze/contour.h
*
* Ray-driven manifold dual contouring of evaluated BRL-CAD geometry.
*/
#ifndef ANALYZE_CONTOUR_H
#define ANALYZE_CONTOUR_H
#include "common.h"
#include <stddef.h>
#include "vmath.h"
#include "analyze/defines.h"
__BEGIN_DECLS
#define ANALYZE_MDC_DEFAULT_MAX_RAYS 0
#define ANALYZE_MDC_DEFAULT_MINIMUM_FREE_MEM 150000000
#define ANALYZE_MDC_DEFAULT_MIN_DEPTH 2
#define ANALYZE_MDC_DEFAULT_MAX_DEPTH 10
#define ANALYZE_MDC_MAX_DEPTH 20
#define ANALYZE_MDC_DEFAULT_MAX_TIME 600
struct db_i;
/** Resource and accuracy controls for analyze_mdc(). */
struct analyze_mdc_params {
/** Requested maximum finest-cell edge length. Zero selects an
* implementation default based on the model bounds. */
fastf_t feature_size;
/** Optional user ceiling for the automatically derived ray budget.
* Zero applies only geometry, depth, time, and memory limits. */
size_t max_rays;
/** Abort before available memory drops below this many bytes. */
size_t minimum_free_mem;
/** Minimum and maximum power-of-two grid depths. */
int min_depth;
int max_depth;
/** Maximum wall-clock processing time in seconds. Zero is unlimited. */
int max_time;
/** Emit progress diagnostics when non-zero. */
int verbosity;
};
#define ANALYZE_MDC_PARAMS_DEFAULT { \
0.0, \
ANALYZE_MDC_DEFAULT_MAX_RAYS, \
ANALYZE_MDC_DEFAULT_MINIMUM_FREE_MEM, \
ANALYZE_MDC_DEFAULT_MIN_DEPTH, \
ANALYZE_MDC_DEFAULT_MAX_DEPTH, \
ANALYZE_MDC_DEFAULT_MAX_TIME, \
0 \
}
enum analyze_mdc_status {
ANALYZE_MDC_OK = 0,
ANALYZE_MDC_INVALID_INPUT = 1,
ANALYZE_MDC_PREP_FAILED = 2,
ANALYZE_MDC_NO_SURFACE = 3,
ANALYZE_MDC_RAY_LIMIT = 4,
ANALYZE_MDC_TIMEOUT = 5,
ANALYZE_MDC_MEMORY_LIMIT = 6,
ANALYZE_MDC_AMBIGUOUS = 7,
ANALYZE_MDC_NOT_MANIFOLD = 8
};
/**
* Evaluate @p object with librt and construct an indexed triangle mesh.
*
* On success, the caller owns @p faces and @p vertices and must release them
* with bu_free(). Failure leaves all outputs empty.
*/
ANALYZE_EXPORT extern enum analyze_mdc_status
analyze_mdc(int **faces, size_t *num_faces, point_t **vertices,
size_t *num_vertices, const char *object, struct db_i *dbip,
const struct analyze_mdc_params *params);
__END_DECLS
#endif /* ANALYZE_CONTOUR_H */
/** @} */
/*
* Local Variables:
* mode: C
* tab-width: 8
* indent-tabs-mode: t
* c-file-style: "stroustrup"
* End:
* ex: shiftwidth=4 tabstop=8
*/
+22 -2
View File
@@ -43,9 +43,29 @@ struct analyze_polygonize_params {
size_t minimum_free_mem;
int verbosity;
};
#define ANALYZE_POLYGONIZE_PARAMS_DEFAULT { 0, 0, 30, 150000000, 0 }
#define ANALYZE_POLYGONIZE_DEFAULT_MAX_CYCLE_TIME 30
#define ANALYZE_POLYGONIZE_DEFAULT_MINIMUM_FREE_MEM 150000000
#define ANALYZE_POLYGONIZE_PARAMS_DEFAULT { 0, 0, \
ANALYZE_POLYGONIZE_DEFAULT_MAX_CYCLE_TIME, \
ANALYZE_POLYGONIZE_DEFAULT_MINIMUM_FREE_MEM, 0 }
/**
* Legacy polygonization interface, implemented by analyze_mdc().
*
* @p size is the requested maximum cell edge length. @p p_s is retained for
* source and binary compatibility but is no longer used as a starting seed.
* The caller owns successful output arrays and must release them with
* bu_free(). Failure leaves all outputs empty.
*
* @return 0 on success
* @return -1 on invalid input or polygonization failure
* @return 2 when a time limit is reached
* @return 3 when the minimum-free-memory limit is reached
*/
ANALYZE_EXPORT extern int
analyze_polygonize(int **faces, int *num_faces, point_t **vertices, int *num_vertices, fastf_t size, point_t p_s, const char *obj, struct db_i *dbip, struct analyze_polygonize_params *p);
analyze_polygonize(int **faces, int *num_faces, point_t **vertices,
int *num_vertices, fastf_t size, point_t p_s, const char *obj,
struct db_i *dbip, struct analyze_polygonize_params *p);
__END_DECLS
+127 -5
View File
@@ -67,6 +67,7 @@
#define BG_SPSR_H
#include "common.h"
#include <stdint.h>
#include "vmath.h"
#include "bg/defines.h"
@@ -109,7 +110,7 @@ struct bg_3d_spsr_opts {
#define BG_3D_SPSR_BOUNDARY_DIRICHLET 3
#define BG_3D_SPSR_DEFAULT_DEGREE 1 /* DEFAULT_FEM_DEGREE */
#define BG_3D_SPSR_DEFAULT_DEPTH 11
#define BG_3D_SPSR_DEFAULT_DEPTH 8
#define BG_3D_SPSR_DEFAULT_KERNELDEPTH 0
#define BG_3D_SPSR_DEFAULT_ITERATIONS 0
#define BG_3D_SPSR_DEFAULT_FULL_DEPTH 0
@@ -117,13 +118,13 @@ struct bg_3d_spsr_opts {
#define BG_3D_SPSR_DEFAULT_BASEVCYCLES 0
#define BG_3D_SPSR_DEFAULT_MAX_MEM 0
#define BG_3D_SPSR_DEFAULT_THREADS 0
#define BG_3D_SPSR_DEFAULT_SAMPLES_PER_NODE 1.1
#define BG_3D_SPSR_DEFAULT_SCALE 1.0
#define BG_3D_SPSR_DEFAULT_SAMPLES_PER_NODE 1.5
#define BG_3D_SPSR_DEFAULT_SCALE 1.1
#define BG_3D_SPSR_DEFAULT_WIDTH 0.0
#define BG_3D_SPSR_DEFAULT_CONFIDENCE 0.0
#define BG_3D_SPSR_DEFAULT_CONFIDENCE_BIAS 0.0
#define BG_3D_SPSR_DEFAULT_CGSOLVER_ACCURACY 1.0e-3
#define BG_3D_SPSR_DEFAULT_POINT_WEIGHT 8.0 /* DefaultPointWeightMultiplier * Degree */
#define BG_3D_SPSR_DEFAULT_POINT_WEIGHT 2.0
#define BG_3D_SPSR_DEFAULT_NONMANIFOLD 0
#define BG_3D_SPSR_DEFAULT_LINEARFIT 0
#define BG_3D_SPSR_DEFAULT_EXACT 1
@@ -167,10 +168,131 @@ struct bg_3d_spsr_opts {
* @return 0 if successful, else error
*
*/
BG_EXPORT int bg_3d_spsr(int **faces, int *num_faces, point_t **vertices, int *num_vertices,
DEPRECATED BG_EXPORT int bg_3d_spsr(int **faces, int *num_faces, point_t **vertices, int *num_vertices,
const point_t *input_points_3d, const vect_t *input_normals_3d,
int num_input_pnts, struct bg_3d_spsr_opts *opts);
/** Reasons the SPSR solver may request additional source samples. */
#define BG_3D_SPSR_REFINE_RESIDUAL 0x01u
#define BG_3D_SPSR_REFINE_NORMAL 0x02u
#define BG_3D_SPSR_REFINE_DENSITY 0x04u
#define BG_3D_SPSR_REFINE_SURFACE_VARIATION 0x08u
/** Adaptive SPSR termination states. */
#define BG_3D_SPSR_COMPLETE 0
#define BG_3D_SPSR_CALLBACK_STOP 1
#define BG_3D_SPSR_POINT_LIMIT 2
#define BG_3D_SPSR_TIME_LIMIT 3
#define BG_3D_SPSR_NO_NEW_SAMPLES 4
#define BG_3D_SPSR_SOLVER_ERROR 5
/**
* One oriented source sample. A positive @p thickness records the length of
* the solid ray partition that supplied the sample. Non-zero equal pair IDs
* identify the entry and exit samples of the same partition.
*/
struct bg_3d_spsr_sample {
point_t point;
vect_t normal;
fastf_t thickness;
uint64_t pair_id;
};
/** A spatial location at which the solver would benefit from more samples. */
struct bg_3d_spsr_refinement_hint {
point_t point;
vect_t normal;
fastf_t error;
unsigned int reasons;
};
/** Source-to-candidate measurements supplied by an adaptive callback. */
struct bg_3d_spsr_validation {
size_t ray_count;
size_t source_hit_rays;
size_t partition_mismatch_rays;
size_t thin_partition_count;
size_t thin_partition_mismatches;
fastf_t endpoint_error_p95;
fastf_t endpoint_error_p99;
fastf_t chord_error_fraction;
fastf_t surface_area_error_fraction;
fastf_t volume_error_fraction;
int passed;
};
/** Information passed to an adaptive source-sampling callback. */
struct bg_3d_spsr_refinement_request {
size_t pass;
fastf_t target_feature_size;
double remaining_time;
const point_t *vertices;
size_t vertex_count;
const int *faces;
size_t face_count;
const struct bg_3d_spsr_refinement_hint *hints;
size_t hint_count;
};
/**
* Callback response. Sample storage remains owned by the callback and need
* only remain valid until the callback returns; libbg copies it immediately.
*/
struct bg_3d_spsr_refinement_response {
const struct bg_3d_spsr_sample *samples;
size_t sample_count;
struct bg_3d_spsr_validation validation;
/** Non-zero asks the adaptive driver to stop after this candidate. */
int stop_refinement;
};
typedef int (*bg_3d_spsr_refinement_func_t)(
struct bg_3d_spsr_refinement_response *response,
const struct bg_3d_spsr_refinement_request *request,
void *client_data);
/** Options controlling a bounded adaptive SPSR reconstruction. */
struct bg_3d_spsr_adaptive_opts {
struct bg_3d_spsr_opts solver;
size_t max_refinement_passes;
size_t max_points;
fastf_t target_feature_size;
double max_time;
};
/** Last-resort solve budget; callbacks may stop earlier when progress stalls. */
#define BG_3D_SPSR_DEFAULT_REFINEMENT_PASSES 8
#define BG_3D_SPSR_ADAPTIVE_OPTS_DEFAULT { \
BG_3D_SPSR_OPTS_DEFAULT, \
BG_3D_SPSR_DEFAULT_REFINEMENT_PASSES, \
0, \
0.0, \
0.0 }
/** Summary of an adaptive SPSR attempt. */
struct bg_3d_spsr_report {
size_t solve_count;
size_t initial_sample_count;
size_t final_sample_count;
size_t requested_sample_count;
size_t accepted_sample_count;
double elapsed_time;
int termination;
struct bg_3d_spsr_validation validation;
};
/**
* Reconstruct a surface and, when requested, obtain targeted additional
* samples from @p refine. A callback is required when refinement passes are
* enabled. The final candidate must be marked as passed by the callback.
*/
BG_EXPORT int bg_3d_spsr_adaptive(int **faces, int *num_faces,
point_t **vertices, int *num_vertices,
const struct bg_3d_spsr_sample *input_samples, size_t sample_count,
const struct bg_3d_spsr_adaptive_opts *opts,
bg_3d_spsr_refinement_func_t refine, void *client_data,
struct bg_3d_spsr_report *report);
__END_DECLS
#endif /* BG_SPSR_H */
+14 -1
View File
@@ -103,10 +103,23 @@ BU_EXPORT extern void bu_nice_set(int newnice);
/**
* Return the maximum number of physical CPUs that are considered to
* be available to this process now.
* be available to this process now, subject to any process-local limit set by
* bu_avail_cpus_set().
*/
BU_EXPORT extern size_t bu_avail_cpus(void);
/**
* Limit the CPU count reported by bu_avail_cpus() and honored by subsequent
* bu_parallel() calls in this process. A value of zero removes the limit.
* Call this before starting worker threads; changing the limit does not resize
* work that is already running.
*
* This is useful when several independent processes share one machine and
* each process contains code that selects its own parallelism with
* bu_avail_cpus().
*/
BU_EXPORT extern void bu_avail_cpus_set(size_t ncpu);
/**
* Create parallel threads of execution.
+68 -2
View File
@@ -227,6 +227,32 @@ struct rt_crofton_params {
double time_ms; /**< wall-clock time budget (ms); 0 = disabled */
};
/** One solid partition observed by a Crofton ray. */
struct rt_crofton_segment {
point_t in_point;
vect_t in_normal;
point_t out_point;
vect_t out_normal;
fastf_t thickness;
size_t ray_id;
};
/**
* Structured samples and estimates produced by rt_crofton_collect().
* Release @p segments with rt_crofton_result_free().
*/
struct rt_crofton_result {
struct rt_crofton_segment *segments;
size_t segment_count;
size_t ray_count;
size_t crossing_count;
double surface_area;
double volume;
};
/** Initialize an rt_crofton_result before first use. */
#define RT_CROFTON_RESULT_INIT { NULL, 0, 0, 0, 0.0, 0.0 }
/**
* Run the Cauchy-Crofton ray-sampling estimator on an already-prepared
@@ -258,6 +284,26 @@ struct rt_crofton_params {
* bound fit failure. A return
* value of 0 means no geometry was intersected by the sampler.
*
* @section crofton_sampler Sampling method
*
* When BRL-CAD is built with BRLCAD_ENABLE_QMC, Crofton sampling defaults to
* OpenQMC's randomized rank-one lattice. It maps four uniformly distributed
* coordinates directly to the two direction and two perpendicular-offset
* coordinates of an oriented line. Directions are isotropic, and the offset
* radius uses R*sqrt(u) so lines are uniform by area across the bounding disk,
* not concentrated toward its center. This is the same invariant line
* measure used by the independent-random-endpoint construction, so the
* Crofton area and volume formulas and stopping criteria do not change.
*
* The lattice improves finite-sample coverage but its samples are correlated,
* unlike ordinary Monte Carlo samples. OpenQMC shuffles indices and applies
* toroidal shifts to mitigate alignment artifacts; a fixed pattern schedule
* keeps runs reproducible. Set the LIBRT_CROFTON_USE_PRNG environment
* variable to a value accepted by bu_str_true() to force the original
* independent-PRNG generator. That path is useful for compatibility and for
* checking whether an unexpected result is specific to lattice correlation.
*
*
* @section crofton_near_tol Near-tolerance sliver geometry: CSG vs BoT divergence
*
* The Cauchy-Crofton formula is mathematically exact for any well-defined
@@ -337,12 +383,32 @@ struct rt_crofton_params {
*/
RT_EXPORT extern int rt_crofton_shoot(double *out_surf_area, double *out_volume, point_t *out_aabb_min, point_t *out_aabb_max, point_t out_obb[8], point_t **out_points, size_t *out_point_count, struct rt_i *rtip, const struct rt_crofton_params *params, const fastf_t *bbox_min, const fastf_t *bbox_max);
/**
* Run Crofton sampling while preserving oriented entry/exit pairs.
*
* @p result must be initialized with RT_CROFTON_RESULT_INIT. On success it
* owns the returned segment array. @p rtip must already be prepared.
* @p ray_offset skips that many samples in the deterministic sequence and
* labels the returned rays starting at that value. This lets successive
* calls use disjoint validation streams.
*/
RT_EXPORT extern int rt_crofton_collect(struct rt_crofton_result *result,
struct rt_i *rtip,
const struct rt_crofton_params *params,
size_t ray_offset,
const fastf_t *bbox_min,
const fastf_t *bbox_max);
/** Release storage owned by a structured Crofton result. */
RT_EXPORT extern void rt_crofton_result_free(
struct rt_crofton_result *result);
/**
* Cauchy-Crofton surface-area and/or volume estimator for a primitive.
*
* Creates a temporary in-memory raytrace of @p ip and fires random chord
* rays until the stopping criteria in @p params are satisfied (or all
* Creates a temporary in-memory raytrace of @p ip and samples Crofton lines
* until the stopping criteria in @p params are satisfied (or all
* criteria are zero / params is NULL, in which case the 2 000-ray default
* is used). Stores the estimated surface area in @p *area (if non-NULL)
* and the estimated volume in @p *vol (if non-NULL).
+1
View File
@@ -258,6 +258,7 @@ function(BRLCAD_Summary)
ext_report("Netpbm" NETPBM_STATUS)
ext_report("OpenCV" OPENCV_STATUS)
ext_report("OpenNURBS" OPENNURBS_STATUS)
ext_report("OpenQMC" OPENQMC_STATUS REQUIRED_VARS "BRLCAD_ENABLE_QMC")
ext_report("OSMesa" OSMESA_STATUS)
ext_report("Portable Network Graphics" PNG_STATUS)
+4
View File
@@ -308,6 +308,10 @@ if(BRLCAD_ENABLE_OPENCL)
endif(NOT OPENCL_FOUND)
endif(BRLCAD_ENABLE_OPENCL)
# Enable quasi-Monte Carlo sampling where supported
option(BRLCAD_ENABLE_QMC "Enable quasi-Monte Carlo sampling" OFF)
mark_as_advanced(BRLCAD_ENABLE_QMC)
# Enable features requiring OpenVDB
option(BRLCAD_ENABLE_OPENVDB "Enable features requiring OpenVDB" OFF)
mark_as_advanced(BRLCAD_ENABLE_OPENVDB)
-8
View File
@@ -45,14 +45,6 @@ cmakefiles(
asciiquack/vendor/cxxopts.hpp
db2adoc/db2adoc.py
db2adoc/db2adoc.xsl
brep_corpus_audit.py
step_corpus_capture.py
step_corpus_run.py
step_corpus_summary.py
step_corpus_verify.py
step_external_corpus_expectations.json
step_local_external_corpus_expectations.json
step_dependency_subset.py
inactvhdrs/CMakeLists.txt
inactvhdrs/inactvhdrs.cpp
)
-619
View File
@@ -1,619 +0,0 @@
#!/usr/bin/env python3
"""Audit BRep wireframe and fast shaded realizations in a directory of .g files.
The companion ``brep-audit`` executable does one BRep per process. This
driver inventories each database, runs those isolated checks, checkpoints
every result to JSON Lines, and writes JSON and Markdown summaries. Geometry
findings do not make the driver fail: they are the intended work list.
"""
import argparse
import datetime
import json
import os
import re
import signal
import subprocess
import sys
import tempfile
import time
from collections import Counter
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
FORMAT = "brlcad-brep-corpus-audit-v1"
HELPER_FORMAT = "brlcad-brep-realization-audit-v1"
def file_identity(path):
try:
info = path.stat()
return {"size": info.st_size, "mtime_ns": info.st_mtime_ns}
except OSError:
return None
def text_tail(value, limit=8000):
return (value or "")[-limit:]
def last_phase(stderr):
phases = re.findall(r"^brep-audit: phase=([a-z]+)$", stderr or "", re.MULTILINE)
return phases[-1] if phases else None
def process_rss_bytes(pid):
"""Read current Linux RSS, returning zero when it is unavailable."""
try:
with open("/proc/{}/status".format(pid), "r", encoding="ascii") as stream:
for line in stream:
if line.startswith("VmRSS:"):
return int(line.split()[1]) * 1024
except (OSError, ValueError, IndexError):
pass
return 0
def kill_process(process):
try:
if os.name == "posix":
os.killpg(process.pid, signal.SIGKILL)
else:
process.kill()
except (OSError, ProcessLookupError):
pass
def file_tail(stream, limit):
stream.flush()
size = os.fstat(stream.fileno()).st_size
stream.seek(max(0, size - limit))
return stream.read().decode("utf-8", errors="replace"), size > limit
def run_process(command, timeout, max_rss_mib=0):
"""Run and monitor one helper, retaining enough detail for diagnostics."""
started = time.monotonic()
with tempfile.TemporaryFile() as stdout_file, \
tempfile.TemporaryFile() as stderr_file:
try:
process = subprocess.Popen(
command,
stdout=stdout_file,
stderr=stderr_file,
start_new_session=(os.name == "posix"),
)
except OSError as error:
return {
"returncode": 127,
"stdout": "",
"stdout_truncated": False,
"stderr": str(error),
"elapsed_seconds": time.monotonic() - started,
"observed_peak_rss_bytes": 0,
"termination_reason": "launch_error",
}
peak_rss = 0
termination_reason = None
while True:
elapsed = time.monotonic() - started
remaining = timeout - elapsed
if remaining <= 0:
termination_reason = "timeout"
kill_process(process)
process.wait()
break
try:
process.wait(timeout=min(0.1, remaining))
peak_rss = max(peak_rss, process_rss_bytes(process.pid))
break
except subprocess.TimeoutExpired:
rss = process_rss_bytes(process.pid)
peak_rss = max(peak_rss, rss)
if max_rss_mib and rss > max_rss_mib * 1024 * 1024:
termination_reason = "rss_limit"
kill_process(process)
process.wait()
break
stdout, stdout_truncated = file_tail(
stdout_file, 16 * 1024 * 1024
)
stderr, _ = file_tail(stderr_file, 64 * 1024)
return {
"returncode": process.returncode,
"stdout": stdout,
"stdout_truncated": stdout_truncated,
"stderr": stderr,
"elapsed_seconds": time.monotonic() - started,
"observed_peak_rss_bytes": peak_rss,
"termination_reason": termination_reason,
}
def synthetic_record(database, object_name, status, issue, run, signature):
return {
"format": HELPER_FORMAT,
"scope": "brep" if object_name else "database",
"database": str(database),
"object": object_name,
"status": status,
"reference": None,
"wireframe": None,
"shaded": None,
"issues": [issue],
"audit_signature": signature,
"database_identity": file_identity(database),
"process": {
"return_code": run["returncode"],
"elapsed_seconds": run["elapsed_seconds"],
"observed_peak_rss_bytes": run["observed_peak_rss_bytes"],
"termination_reason": run["termination_reason"],
"last_phase": last_phase(run["stderr"]),
"stdout_tail": text_tail(run["stdout"]),
"stderr_tail": text_tail(run["stderr"]),
},
}
def parse_helper_json(output):
for line in reversed(output.splitlines()):
if not line.lstrip().startswith("{"):
continue
try:
value = json.loads(line)
except json.JSONDecodeError:
continue
if isinstance(value, dict):
return value
return None
def audit_one(helper, database, object_name, args, signature):
command = [
str(helper),
"--ratio-min", str(args.ratio_min),
"--ratio-max", str(args.ratio_max),
"--tess-abs", str(args.tess_abs),
"--tess-rel", str(args.tess_rel),
"--tess-norm", str(args.tess_norm),
]
if args.address_space_limit_mib:
command.extend([
"--memory-limit-mib", str(args.address_space_limit_mib)
])
command.extend([str(database), object_name])
run = run_process(command, args.timeout, args.max_rss_mib)
if run["termination_reason"] == "timeout":
phase = last_phase(run["stderr"])
return synthetic_record(
database, object_name, "timeout",
"{}_timeout".format(phase) if phase else "process_timeout",
run, signature
)
if run["termination_reason"] == "rss_limit":
phase = last_phase(run["stderr"])
return synthetic_record(
database, object_name, "memory_limit",
"{}_rss_limit_exceeded".format(phase)
if phase else "process_rss_limit_exceeded",
run, signature
)
if run["termination_reason"] == "launch_error":
return synthetic_record(
database, object_name, "error", "helper_launch_failed", run, signature
)
record = parse_helper_json(run["stdout"])
if record is None:
issue = (
"process_signal_{}".format(-run["returncode"])
if run["returncode"] is not None and run["returncode"] < 0
else "helper_output_not_json"
)
return synthetic_record(
database, object_name, "error", issue, run, signature
)
if record.get("format") != HELPER_FORMAT:
return synthetic_record(
database, object_name, "error", "unexpected_helper_format",
run, signature
)
record["scope"] = "brep"
record["audit_signature"] = signature
record["database_identity"] = file_identity(database)
record["process"] = {
"return_code": run["returncode"],
"elapsed_seconds": run["elapsed_seconds"],
"observed_peak_rss_bytes": run["observed_peak_rss_bytes"],
"termination_reason": None,
"last_phase": last_phase(run["stderr"]),
"stdout_tail": "",
"stderr_tail": text_tail(run["stderr"]),
}
expected_return = 0 if record.get("status") == "ok" else 1
if run["returncode"] != expected_return:
record["status"] = "error"
record.setdefault("issues", []).append("helper_exit_status_mismatch")
return record
def list_breps(helper, database, timeout, signature):
run = run_process([str(helper), "--list", str(database)], timeout)
if run["termination_reason"] == "timeout":
return [], synthetic_record(
database, "", "timeout", "brep_inventory_timeout", run, signature
)
if run["returncode"] != 0:
return [], synthetic_record(
database, "", "error", "brep_inventory_failed", run, signature
)
if run["stdout_truncated"]:
return [], synthetic_record(
database, "", "error", "brep_inventory_output_too_large",
run, signature
)
names = sorted(set(line.strip() for line in run["stdout"].splitlines()
if line.strip()))
return names, None
def git_state(start):
try:
revision = subprocess.check_output(
["git", "-C", str(start), "rev-parse", "HEAD"],
stderr=subprocess.DEVNULL, text=True,
).strip()
dirty = bool(subprocess.check_output(
["git", "-C", str(start), "status", "--porcelain"],
stderr=subprocess.DEVNULL, text=True,
).strip())
return revision, dirty
except (OSError, subprocess.CalledProcessError):
return "unknown", None
def read_checkpoint(path, signature):
records = {}
if not path.is_file():
return records
with path.open("r", encoding="utf-8") as stream:
for number, line in enumerate(stream, 1):
try:
record = json.loads(line)
except json.JSONDecodeError:
print("ignoring malformed checkpoint line {} in {}".format(
number, path), file=sys.stderr)
continue
if record.get("audit_signature") != signature:
continue
records[(record.get("database", ""), record.get("object", ""))] = record
return records
def all_issues(record):
issues = list(record.get("issues") or [])
for mode in ("wireframe", "shaded"):
result = record.get(mode)
if result:
issues.extend(result.get("issues") or [])
return list(dict.fromkeys(str(issue) for issue in issues))
def format_vector(values):
if not values:
return "-"
return " × ".join("{:.5g}".format(value) for value in values)
def format_geometry(result, primitive):
if not result:
return "-"
ratios = result.get("dimension_ratios")
ratio_text = ""
if ratios:
ratio_text = "; ratios " + "/".join(
"-" if value is None else "{:.3g}".format(value)
for value in ratios
)
return "{} points, {} {}{}".format(
result.get("vertices", 0), result.get("primitives", 0),
primitive, ratio_text,
)
def markdown_escape(value):
return str(value).replace("|", "\\|").replace("\n", "<br>")
def worklist_markdown(summary, input_directory):
totals = summary["totals"]
lines = [
"# BRep realization audit",
"",
"Revision: `{}`{}".format(
summary["revision"], " (dirty)" if summary.get("dirty") else ""
),
"",
(
"{} databases, {} BReps: {} okay, {} geometry findings, "
"{} timeouts, {} memory-limit stops, and {} process errors."
).format(
totals["databases"], totals["breps"], totals["ok"], totals["fail"],
totals["timeout"], totals["memory_limit"], totals["error"],
),
"",
(
"Accepted generated/reference dimension ratios: {}–{}. "
"Shaded tessellation tolerance: abs {}, rel {}, norm {}."
).format(
summary["options"]["ratio_min"], summary["options"]["ratio_max"],
summary["options"]["tess_abs"], summary["options"]["tess_rel"],
summary["options"]["tess_norm"],
),
"",
"## Work list",
"",
]
findings = [
record for record in summary["results"]
if record.get("status") != "ok"
]
if not findings:
lines.append("No findings.")
return "\n".join(lines) + "\n"
lines.extend([
"| Database | BRep | Status | Reference dimensions | Wireframe | Shaded | Peak RSS MiB | Issues |",
"|---|---|---|---|---|---|---:|---|",
])
for record in findings:
database = Path(record.get("database", ""))
try:
database_name = str(database.relative_to(input_directory))
except ValueError:
database_name = str(database)
wire = record.get("wireframe")
shaded = record.get("shaded")
peaks = [
int(result.get("peak_rss_bytes") or 0)
for result in (wire, shaded) if result
]
process = record.get("process") or {}
peaks.append(int(process.get("observed_peak_rss_bytes") or 0))
reference = record.get("reference") or {}
lines.append("| {} | {} | {} | {} | {} | {} | {:.1f} | {} |".format(
markdown_escape(database_name),
markdown_escape(record.get("object") or "(inventory)"),
markdown_escape(record.get("status", "error")),
markdown_escape(format_vector(reference.get("dimensions"))),
markdown_escape(format_geometry(wire, "segments")),
markdown_escape(format_geometry(shaded, "triangles")),
max(peaks, default=0) / (1024.0 * 1024.0),
markdown_escape(", ".join(all_issues(record)) or "unspecified"),
))
return "\n".join(lines) + "\n"
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("input_directory", type=Path)
parser.add_argument("output_directory", type=Path)
parser.add_argument(
"--brep-audit", type=Path, default=Path(".build/bin/brep-audit")
)
parser.add_argument("-j", "--jobs", type=int, default=1,
help="BRep processes to run concurrently (default: 1)")
parser.add_argument("--timeout", type=float, default=300.0,
help="seconds allowed per BRep")
parser.add_argument("--list-timeout", type=float, default=120.0,
help="seconds allowed to inventory one database")
parser.add_argument("--ratio-min", type=float, default=0.5)
parser.add_argument("--ratio-max", type=float, default=2.0)
parser.add_argument("--tess-abs", type=float, default=0.0)
parser.add_argument("--tess-rel", type=float, default=0.01)
parser.add_argument("--tess-norm", type=float, default=0.0)
parser.add_argument(
"--max-rss-mib", type=int, default=0,
help="kill a BRep helper above this observed RSS (zero disables)"
)
parser.add_argument(
"--address-space-limit-mib", type=int, default=0,
help="set the helper RLIMIT_AS in MiB (zero disables)"
)
parser.add_argument("--only-file",
help="regular expression matched against .g filenames")
parser.add_argument("--only-object",
help="regular expression matched against BRep names")
parser.add_argument("--resume", action="store_true",
help="retain checkpointed results with identical options")
args = parser.parse_args()
if args.jobs < 1:
parser.error("--jobs must be positive")
if args.timeout <= 0 or args.list_timeout <= 0:
parser.error("timeouts must be positive")
if args.ratio_min <= 0 or args.ratio_max < args.ratio_min:
parser.error("dimension ratio limits are invalid")
if min(args.tess_abs, args.tess_rel, args.tess_norm) < 0:
parser.error("tessellation tolerances cannot be negative")
if args.max_rss_mib < 0 or args.address_space_limit_mib < 0:
parser.error("memory limits cannot be negative")
input_directory = args.input_directory.resolve()
output_directory = args.output_directory.resolve()
helper = args.brep_audit.resolve()
if not helper.is_file():
parser.error("brep-audit not found: {}".format(helper))
file_matcher = re.compile(args.only_file) if args.only_file else None
object_matcher = re.compile(args.only_object) if args.only_object else None
databases = sorted(
path.resolve() for path in input_directory.iterdir()
if path.is_file() and path.suffix.lower() == ".g" and
(not file_matcher or file_matcher.search(path.name))
)
if not databases:
parser.error("no matching .g files in {}".format(input_directory))
options = {
"ratio_min": args.ratio_min,
"ratio_max": args.ratio_max,
"tess_abs": args.tess_abs,
"tess_rel": args.tess_rel,
"tess_norm": args.tess_norm,
"timeout": args.timeout,
"list_timeout": args.list_timeout,
"max_rss_mib": args.max_rss_mib,
"address_space_limit_mib": args.address_space_limit_mib,
"only_file": args.only_file,
"only_object": args.only_object,
"helper": str(helper),
"helper_size": helper.stat().st_size,
"helper_mtime_ns": helper.stat().st_mtime_ns,
}
signature = options
output_directory.mkdir(parents=True, exist_ok=True)
checkpoint_path = output_directory / "brep-audit-results.jsonl"
records = (
read_checkpoint(checkpoint_path, signature) if args.resume else {}
)
tasks = []
for index, database in enumerate(databases, 1):
print("[inventory {}/{}] {}".format(
index, len(databases), database.name), flush=True)
names, error_record = list_breps(
helper, database, args.list_timeout, signature
)
if error_record:
for key in list(records):
if key[0] == str(database):
records.pop(key)
records[(str(database), "")] = error_record
continue
records.pop((str(database), ""), None)
if object_matcher:
names = [name for name in names if object_matcher.search(name)]
current_names = set(names)
identity = file_identity(database)
for key, record in list(records.items()):
if key[0] != str(database):
continue
if key[1] not in current_names or \
record.get("database_identity") != identity:
records.pop(key)
tasks.extend((database, name) for name in names)
# Rewrite a resumed checkpoint with only the selected run signature, then
# append each new result immediately so an interrupted corpus run is usable.
with checkpoint_path.open("w", encoding="utf-8") as checkpoint:
for record in sorted(
records.values(),
key=lambda item: (item.get("database", ""), item.get("object", ""))):
checkpoint.write(json.dumps(record, sort_keys=True) + "\n")
checkpoint.flush()
pending = [
task for task in tasks if (str(task[0]), task[1]) not in records
]
resumed = len(tasks) - len(pending)
if resumed:
print("resuming {} completed BRep records".format(resumed), flush=True)
completed = 0
with checkpoint_path.open("a", encoding="utf-8") as checkpoint:
with ThreadPoolExecutor(max_workers=args.jobs) as pool:
futures = {
pool.submit(
audit_one, helper, database, object_name, args, signature
): (database, object_name)
for database, object_name in pending
}
for future in as_completed(futures):
database, object_name = futures[future]
try:
record = future.result()
except Exception as error: # Preserve the rest of a corpus run.
run = {
"returncode": 125,
"stdout": "",
"stderr": repr(error),
"elapsed_seconds": 0.0,
"observed_peak_rss_bytes": 0,
"termination_reason": "driver_error",
}
record = synthetic_record(
database, object_name, "error",
"audit_driver_exception", run, signature
)
records[(str(database), object_name)] = record
checkpoint.write(json.dumps(record, sort_keys=True) + "\n")
checkpoint.flush()
completed += 1
print("[audit {}/{}] {}:{} {}".format(
completed, len(pending), database.name, object_name,
record["status"],
), flush=True)
results = sorted(
records.values(),
key=lambda item: (item.get("database", ""), item.get("object", "")),
)
statuses = Counter(
record.get("status", "error")
for record in results if record.get("scope") == "brep"
)
database_errors = [
record for record in results if record.get("scope") == "database"
]
revision, dirty = git_state(Path(__file__).resolve().parents[2])
summary = {
"format": FORMAT,
"generated_utc": datetime.datetime.now(
datetime.timezone.utc
).isoformat(),
"revision": revision,
"dirty": dirty,
"input_directory": str(input_directory),
"output_directory": str(output_directory),
"helper": str(helper),
"options": options,
"totals": {
"databases": len(databases),
"database_inventory_errors": len(database_errors),
"breps": sum(statuses.values()),
"ok": statuses["ok"],
"fail": statuses["fail"],
"timeout": statuses["timeout"],
"memory_limit": statuses["memory_limit"],
"error": statuses["error"] + len(database_errors),
},
"results": results,
}
summary_path = output_directory / "brep-audit-summary.json"
worklist_path = output_directory / "brep-audit-worklist.md"
summary_path.write_text(
json.dumps(summary, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
worklist_path.write_text(
worklist_markdown(summary, input_directory), encoding="utf-8"
)
totals = summary["totals"]
print(
"audited {} BReps: {} okay; {} work items; reports in {}".format(
totals["breps"], totals["ok"],
totals["breps"] - totals["ok"] + len(database_errors),
output_directory,
)
)
return 0
if __name__ == "__main__":
sys.exit(main())
-127
View File
@@ -1,127 +0,0 @@
#!/usr/bin/env python3
"""Capture standard swrast overview images for converted STEP databases.
Each database is opened in its own gsh process so the display manager and GED
state are isolated. The two modes are written to separate directories and
use the database filename (without its final ``.g``) as the image name.
"""
import argparse
import json
import os
import re
import subprocess
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
def image_stem(path):
return path.name[:-2] if path.name.lower().endswith(".g") else path.stem
def capture(gsh, model, output, mode, azimuth, elevation, timeout):
output.mkdir(parents=True, exist_ok=True)
image = output / (image_stem(model) + ".png")
try:
tops = subprocess.run(
[str(gsh), "--new-cmds", str(model), "tops"],
text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
timeout=timeout,
).stdout.splitlines()
except subprocess.TimeoutExpired:
return {"model": str(model), "mode": mode, "image": str(image),
"returncode": 124, "complete": False,
"log": "timed out while listing top-level objects"}
tops = [line.strip().rstrip("/") for line in tops
if line.strip().endswith("/") and line.strip().count("/") == 1]
if not tops:
return {"model": str(model), "mode": mode, "image": str(image),
"returncode": 1, "complete": False,
"log": "database has no top-level objects"}
commands = [
"dm attach swrast SWDM",
"draw -m{} {}".format(mode, " ".join(tops)),
"autoview",
"ae {} {}".format(azimuth, elevation),
"screengrab -D SWDM {}".format(image),
"quit",
]
try:
proc = subprocess.run(
[str(gsh), "--new-cmds", str(model)],
input="\n".join(commands) + "\n",
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
timeout=timeout,
)
except subprocess.TimeoutExpired as error:
return {"model": str(model), "mode": mode, "image": str(image),
"returncode": 124, "complete": False,
"log": str(error.stdout or "")[-4000:] + "\ntimed out during capture"}
return {
"model": str(model),
"mode": mode,
"image": str(image),
"returncode": proc.returncode,
"complete": proc.returncode == 0 and image.is_file() and image.stat().st_size > 0,
"log": proc.stdout[-4000:],
}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("input_directory", type=Path)
parser.add_argument("output_directory", type=Path)
parser.add_argument("--gsh", type=Path, default=Path(".build/bin/gsh"))
parser.add_argument("--jobs", type=int, default=min(2, os.cpu_count() or 1))
parser.add_argument("--timeout", type=int, default=300,
help="maximum seconds per model and view")
parser.add_argument("--mode", choices=("wireframe", "shaded", "both"), default="both")
parser.add_argument("--only", help="regular expression matched against model names")
parser.add_argument("--azimuth", type=float, default=35.0)
parser.add_argument("--elevation", type=float, default=25.0)
args = parser.parse_args()
if args.jobs < 1:
parser.error("--jobs must be positive")
if args.timeout < 1:
parser.error("--timeout must be positive")
if not args.gsh.is_file():
parser.error("gsh not found: {}".format(args.gsh))
matcher = re.compile(args.only) if args.only else None
models = sorted(path for path in args.input_directory.glob("*.g")
if not matcher or matcher.search(path.name))
if not models:
parser.error("no converted .g models in {}".format(args.input_directory))
modes = [("wireframe", 0), ("shaded", 1)] if args.mode == "both" else [
(args.mode, 0 if args.mode == "wireframe" else 1)
]
jobs = []
for name, mode in modes:
target = args.output_directory / name
jobs.extend((model, target, mode) for model in models)
with ThreadPoolExecutor(max_workers=args.jobs) as pool:
results = list(pool.map(
lambda item: capture(args.gsh, item[0], item[1], item[2],
args.azimuth, args.elevation, args.timeout), jobs
))
args.output_directory.mkdir(parents=True, exist_ok=True)
manifest = {
"format": "brlcad-step-corpus-capture-v1",
"azimuth": args.azimuth,
"elevation": args.elevation,
"jobs": args.jobs,
"results": results,
}
(args.output_directory / "capture-manifest.json").write_text(
json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8"
)
failed = [item for item in results if not item["complete"]]
print("captured {}/{} images".format(len(results) - len(failed), len(results)))
return 1 if failed else 0
if __name__ == "__main__":
raise SystemExit(main())
-501
View File
@@ -1,501 +0,0 @@
#!/usr/bin/env python3
"""Run the schema-dispatching BRL-CAD STEP importer over an external corpus.
Unknown schemas are recorded in the run manifest but are not passed to the
converter. Each supported input gets a structured report, a diagnostic log,
and a tab-separated status record consumed by ``step_corpus_summary.py``.
Nested input directories are scanned recursively, and existing complete
records may be resumed.
"""
import argparse
import datetime
import hashlib
import json
import os
import platform
import re
import signal
import subprocess
import sys
import time
from pathlib import Path
STEP_SUFFIXES = {".stp", ".step", ".p21"}
INCOMPLETE_PROCESS_STATUSES = {124, 130, 137, 143}
def schema_names(path):
with path.open("rb") as stream:
header = stream.read(2 * 1024 * 1024).decode("latin-1", errors="replace")
match = re.search(
r"FILE_SCHEMA\s*\(\s*\((.*?)\)\s*\)", header,
re.IGNORECASE | re.DOTALL,
)
if not match:
return []
return [
value.replace("''", "'").upper()
for value in re.findall(r"'((?:''|[^'])*)'", match.group(1))
]
def schema_family(names):
"""Mirror the public host's supported-family and interim-profile routing."""
families = set()
legacy_ap203e2 = {
"CONFIGURATION_CONTROL_3D_DESIGN_MIM_LF",
"CCD_CLA_GVP_AST",
"CCD_CLA_GVP_AST_ASD",
}
interim_companions = (
"GEOMETRIC_VALIDATION_PROPERTIES_MIM",
"GEOMETRIC_VALIDATION_PROPERTY_REPRESENTATION_MIM",
"SHAPE_APPEARANCE_LAYER_MIM",
"SHAPE_APPEARANCE_LAYERS_MIM",
)
for name in names:
if "AP203_CONFIGURATION_CONTROLLED" in name or any(
name.startswith(alias) for alias in legacy_ap203e2):
families.add("AP203e2")
elif "AUTOMOTIVE_DESIGN" in name:
families.add("AP214")
elif "CONFIG_CONTROL_DESIGN" in name:
families.add("AP203")
elif "AP242" in name or "MANAGED_MODEL_BASED_3D_ENGINEERING" in name:
families.add("AP242")
if families == {"AP203"} and any(
name.startswith(companion)
for name in names for companion in interim_companions):
return "AP203e2"
return next(iter(families)) if len(families) == 1 else "unsupported"
def artifact_stem(path, input_directory):
relative = path.relative_to(input_directory).as_posix()
readable = re.sub(r"[^A-Za-z0-9_.-]+", "_", relative)
# Sanitizing path separators and punctuation is not injective. Retain a
# readable prefix while making nested and similarly named inputs safe.
identity = hashlib.sha256(relative.encode("utf-8")).hexdigest()[:12]
return "{}__{}_".format(readable, identity)
def legacy_artifact_stem(path, input_directory):
"""Return the pre-v2 stem so an in-progress corpus can be resumed."""
relative = path.relative_to(input_directory).as_posix()
return re.sub(r"[^A-Za-z0-9_.-]+", "_", relative) + "_"
def completed_status(path):
try:
status = int(path.read_text(encoding="utf-8").split("\t", 1)[0])
except (OSError, ValueError):
return False
return status not in INCOMPLETE_PROCESS_STATUSES
def synthetic_report(input_path, status, message):
return {
"format": "brlcad-step-import-report-v1",
"input": str(input_path),
"output": "",
"exit_status": status,
"coverage": {
"geometry_attempted": 0,
"geometry_written": 0,
"geometry_skipped": 0,
},
"validation": {
"invalid_breps": 0,
"output_failures": 0,
"repairs": 0,
"inferred_curves": 0,
},
"diagnostics": [{
"severity": "error",
"entity_id": 0,
"entity_type": "",
"attribute": "",
"message": message,
"count": 1,
"aggregated": False,
}],
"timings_us": {},
"peak_rss_bytes": 0,
"skipped_items": [],
"corpus_run_incomplete": True,
}
def file_sha256(path):
digest = hashlib.sha256()
with path.open("rb") as stream:
for block in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def executable_identity(path):
stat = path.stat()
return {
"path": str(path),
"size": stat.st_size,
"mtime_ns": stat.st_mtime_ns,
"sha256": file_sha256(path),
}
def schema_plugin_paths(step_g):
"""Locate build/install-tree schema modules used by the plugin host."""
plugin_directory = step_g.parent.parent / "libexec" / "step"
if not plugin_directory.is_dir():
return []
return sorted(
path for path in plugin_directory.glob("libstep-schema-*")
if path.is_file()
)
def schema_plugin_identities(step_g):
"""Identify build/install-tree schema modules used by the plugin host."""
return [executable_identity(path) for path in schema_plugin_paths(step_g)]
def linked_library_identities(executables, runtime_root):
"""Hash resolved runtime libraries supplied by the selected build tree.
Converter and plugin hashes do not detect a rebuilt geometry or STEPcode
shared library. On platforms with ``ldd``, retain every resolved library
below the executable's build/install root so a long run cannot be mistaken
for a homogeneous baseline after one of those dependencies changes.
"""
libraries = {}
for executable in executables:
try:
result = subprocess.run(
["ldd", str(executable)], check=False, capture_output=True,
text=True,
)
except OSError:
return []
if result.returncode:
continue
for line in result.stdout.splitlines():
match = re.search(r"=>\s+(/\S+)", line)
if not match:
match = re.match(r"\s*(/\S+)", line)
if not match:
continue
path = Path(match.group(1)).resolve()
try:
path.relative_to(runtime_root)
except ValueError:
continue
if path.is_file():
libraries[str(path)] = path
return [executable_identity(libraries[name]) for name in sorted(libraries)]
def host_snapshot():
"""Capture lightweight run context without adding a monitoring dependency."""
snapshot = {
"utc": datetime.datetime.now(datetime.timezone.utc).replace(
microsecond=0
).isoformat(),
}
try:
snapshot["load_average"] = list(os.getloadavg())
except (AttributeError, OSError):
pass
try:
values = {}
with Path("/proc/meminfo").open("r", encoding="ascii") as stream:
for line in stream:
key, value = line.split(":", 1)
if key in ("MemTotal", "MemAvailable"):
values[key] = int(value.split()[0]) * 1024
if values:
snapshot["memory_bytes"] = values
except (OSError, ValueError):
pass
return snapshot
def cpu_model():
try:
with Path("/proc/cpuinfo").open("r", encoding="ascii") as stream:
for line in stream:
if line.lower().startswith("model name"):
return line.split(":", 1)[1].strip()
except (OSError, IndexError):
pass
return platform.processor()
def write_manifest(path, manifest):
"""Atomically preserve progress so an interrupted long run is auditable."""
temporary = path.with_suffix(path.suffix + ".tmp")
temporary.write_text(
json.dumps(manifest, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
temporary.replace(path)
def terminate_process(process, force=False):
try:
if os.name == "nt":
process.kill() if force else process.terminate()
else:
os.killpg(process.pid, signal.SIGKILL if force else signal.SIGTERM)
except ProcessLookupError:
pass
def run_converter(command, log, timeout):
"""Run one converter and terminate its complete process group on timeout."""
process = subprocess.Popen(
command, stdout=log, stderr=subprocess.STDOUT,
start_new_session=os.name != "nt",
)
try:
return process.wait(timeout=timeout if timeout > 0 else None), False
except subprocess.TimeoutExpired:
terminate_process(process)
try:
process.wait(timeout=30)
except subprocess.TimeoutExpired:
terminate_process(process, force=True)
process.wait()
return 124, True
except BaseException:
terminate_process(process)
try:
process.wait(timeout=30)
except subprocess.TimeoutExpired:
terminate_process(process, force=True)
process.wait()
raise
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("input_directory", type=Path)
parser.add_argument("output_directory", type=Path)
parser.add_argument("--step-g", type=Path, default=Path(".build/bin/step-g"))
parser.add_argument("-j", "--jobs", type=int, default=min(12, os.cpu_count() or 1),
help="geometry workers inside each converter")
parser.add_argument("--previous", type=Path,
help="previous corpus-summary.json for trend deltas")
parser.add_argument("--expectations", type=Path,
help="checksum-pinned corpus expectation manifest")
parser.add_argument("--allow-missing-expectations", action="store_true",
help="allow expectation entries absent from a filtered run")
parser.add_argument("--run-id", help="stable label for this run")
parser.add_argument("--resume", action="store_true")
parser.add_argument("--exact", action="store_true")
parser.add_argument("--timeout", type=float, default=0.0,
help="outer wall-clock limit per file in seconds (0 disables)")
parser.add_argument("--stall-timeout", type=float,
help="override step-g's no-progress timeout in seconds")
parser.add_argument(
"--only", help="regular expression matched against relative input paths"
)
parser.add_argument("--list", action="store_true",
help="classify inputs without running converters")
args = parser.parse_args()
if args.jobs < 1 or args.timeout < 0:
parser.error("--jobs must be positive and --timeout must be nonnegative")
if args.stall_timeout is not None and args.stall_timeout <= 0:
parser.error("--stall-timeout must be positive")
input_directory = args.input_directory.resolve()
output_directory = args.output_directory.resolve()
output_directory.mkdir(parents=True, exist_ok=True)
matcher = re.compile(args.only) if args.only else None
inputs = sorted(
path for path in input_directory.rglob("*")
if path.is_file() and path.suffix.lower() in STEP_SUFFIXES and
(not matcher or matcher.search(path.relative_to(input_directory).as_posix()))
)
if not inputs:
parser.error("no matching STEP files in {}".format(input_directory))
run_id = args.run_id or datetime.datetime.now(
datetime.timezone.utc
).strftime("%Y%m%dT%H%M%SZ")
step_g = args.step_g.resolve()
if not step_g.is_file():
parser.error("step-g not found: {}".format(step_g))
plugin_paths = schema_plugin_paths(step_g)
runtime_root = step_g.parent.parent.resolve()
manifest = {
"format": "brlcad-step-corpus-run-v1",
"run_id": run_id,
"started_utc": datetime.datetime.now(datetime.timezone.utc).replace(
microsecond=0
).isoformat(),
"host": {
"hostname": platform.node(),
"platform": platform.platform(),
"machine": platform.machine(),
"cpu_model": cpu_model(),
"logical_cpus": os.cpu_count(),
"initial_snapshot": host_snapshot(),
},
"input_directory": str(input_directory),
"output_directory": str(output_directory),
"options": {
"jobs": args.jobs,
"exact": args.exact,
"recursive": True,
"timeout_seconds": args.timeout,
"stall_timeout_seconds": args.stall_timeout,
},
"step_g": executable_identity(step_g),
"schema_plugins": [executable_identity(path) for path in plugin_paths],
"linked_libraries": linked_library_identities(
[step_g] + plugin_paths, runtime_root
),
"expectations": str(args.expectations.resolve()) if args.expectations else None,
"files": [],
}
manifest_path = output_directory / "run-manifest.json"
write_manifest(manifest_path, manifest)
for index, input_path in enumerate(inputs, 1):
relative_input = input_path.relative_to(input_directory).as_posix()
declared_schemas = schema_names(input_path)
declared_schema = declared_schemas[0] if declared_schemas else ""
family = schema_family(declared_schemas)
record = {
"input": str(input_path),
"declared_schema": declared_schema,
"declared_schemas": declared_schemas,
"schema": family,
}
if args.list:
print("{}\t{}\t{}".format(
family, ";".join(declared_schemas), relative_input
))
manifest["files"].append(record)
write_manifest(manifest_path, manifest)
continue
input_stat = input_path.stat()
record["input_identity"] = {
"size": input_stat.st_size,
"mtime_ns": input_stat.st_mtime_ns,
"sha256": file_sha256(input_path),
}
if family == "unsupported":
record["result"] = "skipped"
record["reason"] = "unrecognized schema"
manifest["files"].append(record)
write_manifest(manifest_path, manifest)
print("[{}/{}] skip {} ({})".format(
index, len(inputs), relative_input, family
))
continue
stem = artifact_stem(input_path, input_directory)
report_path = output_directory / (stem + ".json")
log_path = output_directory / (stem + ".log")
status_path = output_directory / (stem + ".status")
if args.resume:
legacy_stem = legacy_artifact_stem(input_path, input_directory)
legacy_report = output_directory / (legacy_stem + ".json")
legacy_status = output_directory / (legacy_stem + ".status")
if legacy_report.is_file() and completed_status(legacy_status):
stem = legacy_stem
report_path = legacy_report
log_path = output_directory / (stem + ".log")
status_path = legacy_status
if args.resume and report_path.is_file() and completed_status(status_path):
record["result"] = "resumed"
record["report"] = report_path.name
manifest["files"].append(record)
write_manifest(manifest_path, manifest)
print("[{}/{}] keep {}".format(index, len(inputs), relative_input))
continue
command = [
str(step_g), "-D", "-j", str(args.jobs),
"--report", str(report_path),
]
if args.exact:
command.append("--exact")
if args.stall_timeout is not None:
command.extend(["--stall-timeout", str(args.stall_timeout)])
command.append(str(input_path))
print("[{}/{}] {} {}".format(index, len(inputs), family, relative_input),
flush=True)
record.update({"result": "running", "host_before": host_snapshot()})
manifest["files"].append(record)
write_manifest(manifest_path, manifest)
started = time.monotonic()
with log_path.open("wb") as log:
try:
status, timed_out = run_converter(command, log, args.timeout)
message = "converter exited without writing a structured report"
if timed_out:
message = "converter exceeded the {:.1f}-second outer timeout".format(
args.timeout
)
except OSError as error:
status = 127
timed_out = False
message = "could not execute converter: {}".format(error)
elapsed = time.monotonic() - started
if not report_path.is_file():
report_path.write_text(
json.dumps(synthetic_report(input_path, status, message), indent=2) + "\n",
encoding="utf-8",
)
status_path.write_text(
"{}\t{:.3f}\t{}\t{}\n".format(status, elapsed, family, input_path),
encoding="utf-8",
)
record.update({
"result": "completed",
"process_exit_status": status,
"elapsed_seconds": elapsed,
"timed_out": timed_out,
"report": report_path.name,
"log": log_path.name,
"host_after": host_snapshot(),
})
write_manifest(manifest_path, manifest)
manifest["finished_utc"] = datetime.datetime.now(
datetime.timezone.utc
).replace(microsecond=0).isoformat()
manifest["host"]["final_snapshot"] = host_snapshot()
write_manifest(manifest_path, manifest)
if not args.list:
summary_command = [
sys.executable,
str(Path(__file__).with_name("step_corpus_summary.py")),
str(output_directory),
"--run-id", run_id,
]
if args.previous:
summary_command.extend(["--previous", str(args.previous.resolve())])
summary_status = subprocess.call(summary_command)
if summary_status or not args.expectations:
return summary_status
verify_command = [
sys.executable,
str(Path(__file__).with_name("step_corpus_verify.py")),
str(output_directory),
str(args.expectations.resolve()),
]
if args.allow_missing_expectations:
verify_command.append("--allow-missing")
return subprocess.call(verify_command)
return 0
if __name__ == "__main__":
sys.exit(main())
-939
View File
@@ -1,939 +0,0 @@
#!/usr/bin/env python3
"""Summarize one BRL-CAD STEP converter corpus run and compare it to another.
The corpus runner writes one ``NAME.status`` record and one ``NAME.json``
converter report per input. A status record is tab separated::
exit_status<TAB>elapsed_seconds<TAB>schema<TAB>input_path
This utility intentionally consumes the converters' structured reports rather
than scraping diagnostics. It writes a durable JSON record suitable for trend
plots and a compact Markdown companion for review.
"""
import argparse
import datetime
import json
import math
import os
import statistics
import subprocess
import sys
from collections import Counter
from pathlib import Path
FORMAT = "brlcad-step-corpus-summary-v2"
INCOMPLETE_PROCESS_STATUSES = {124, 130, 137, 143}
def run_complete(item):
"""Return whether an input reached a normal converter terminal state."""
if "run_complete" in item:
return bool(item["run_complete"])
return int(item.get("process_exit_status", 0)) not in INCOMPLETE_PROCESS_STATUSES
def read_json(path):
with path.open("r", encoding="utf-8") as stream:
return json.load(stream)
def git_state(start):
try:
revision = subprocess.check_output(
["git", "-C", str(start), "rev-parse", "HEAD"],
stderr=subprocess.DEVNULL,
text=True,
).strip()
dirty = bool(
subprocess.check_output(
["git", "-C", str(start), "status", "--porcelain"],
stderr=subprocess.DEVNULL,
text=True,
).strip()
)
return revision, dirty
except (OSError, subprocess.CalledProcessError):
return "unknown", None
def failure_category(reason):
text = (reason or "").lower()
if any(word in text for word in ("budget", "expired", "timed out", "timeout")):
return "work_budget"
if any(word in text for word in (
"not solid", "non-solid", "solid validation", "validate as a solid"
)):
return "non_solid"
if "structural" in text:
return "structural"
if "exact" in text or "conversion failed" in text:
return "exact_conversion"
if "opennurbs" in text:
return "structural"
if "no importer" in text or "unsupported" in text:
return "unsupported"
if "output" in text or "database" in text or "write" in text:
return "output"
return "other"
def parse_status(path):
fields = path.read_text(encoding="utf-8").rstrip("\n").split("\t", 3)
if len(fields) != 4:
raise ValueError("{} is not a four-field corpus status record".format(path))
return {
"process_exit_status": int(fields[0]),
"elapsed_seconds": float(fields[1]),
"schema": fields[2],
"input": fields[3],
}
def occurrence_depth(occurrences):
"""Return the maximum product-assembly depth represented by occurrences."""
children = {}
vertices = set()
for occurrence in occurrences:
parent = int(occurrence.get("parent_product_id") or 0)
child = int(occurrence.get("child_product_id") or 0)
if parent <= 0 or child <= 0:
continue
children.setdefault(parent, set()).add(child)
vertices.update((parent, child))
active = set()
memo = {}
def visit(vertex):
if vertex in memo:
return memo[vertex]
if vertex in active:
# A cycle is invalid, but retain a useful finite metric and let the
# converter's structured diagnostics report the underlying issue.
return 0
active.add(vertex)
depth = max((1 + visit(child)
for child in children.get(vertex, ())), default=0)
active.remove(vertex)
memo[vertex] = depth
return depth
return max((visit(vertex) for vertex in vertices), default=0)
def report_record(status_path):
status = parse_status(status_path)
report_path = status_path.with_suffix(".json")
if not report_path.is_file():
raise ValueError("missing report paired with {}".format(status_path))
report = read_json(report_path)
coverage = report.get("coverage") or {}
validation = report.get("validation") or {}
timings = report.get("timings_us") or {}
performance = report.get("performance") or {}
calibration = performance.get("calibration") or {}
stage_timings = performance.get("stages") or {}
skipped = report.get("skipped_items") or []
occurrences = report.get("occurrence_details") or []
configuration_records = report.get("configuration_records") or []
representation_coverage = report.get("representation_coverage") or []
attempted = int(coverage.get("geometry_attempted") or 0)
written = int(coverage.get("geometry_written") or 0)
skipped_count = int(coverage.get("geometry_skipped") or len(skipped))
categories = Counter(failure_category(item.get("reason")) for item in skipped)
invalid_breps = int(validation.get("invalid_breps") or 0)
invalid_breps_written = int(validation.get("invalid_breps_written") or 0)
invalid_breps_rejected = int(validation.get("invalid_breps_rejected") or 0)
output_failures = int(validation.get("output_failures") or 0)
inferred_curves = int(validation.get("inferred_curves") or 0)
if invalid_breps_written:
categories["invalid_brep_preserved"] += invalid_breps_written
if output_failures:
categories["output"] += output_failures
configuration_types = Counter(
item.get("type") or "UNKNOWN" for item in configuration_records
)
representation_statuses = Counter(
item.get("status") or "unknown" for item in representation_coverage
)
converter_status = int(
report.get("exit_status", status["process_exit_status"])
)
intentionally_empty = (
status["process_exit_status"] == 6 and converter_status == 6 and
attempted == 0 and bool(representation_coverage) and
all(item.get("status") == "intentionally_non_geometric"
for item in representation_coverage)
)
input_path = str(report.get("input") or status["input"])
result = {
"name": os.path.basename(input_path),
"input": input_path,
"schema": status["schema"],
"report": report_path.name,
"process_exit_status": status["process_exit_status"],
"converter_exit_status": converter_status,
"run_complete": status["process_exit_status"] not in
INCOMPLETE_PROCESS_STATUSES,
"elapsed_seconds": status["elapsed_seconds"],
"geometry_attempted": attempted,
"geometry_written": written,
"geometry_skipped": skipped_count,
"success_fraction": (float(written) / attempted) if attempted else 0.0,
"products": int(coverage.get("products") or 0),
"occurrences": int(coverage.get("occurrences") or 0),
"assembly_usage_count": len(report.get("assembly_usages") or []),
"occurrence_detail_count": len(occurrences),
"occurrence_max_depth": occurrence_depth(occurrences),
"occurrence_methods": sorted({
item.get("shape_method") or "unknown" for item in occurrences
}),
"styles_extracted": int(coverage.get("styles_extracted") or 0),
"styles_applied": int(coverage.get("styles_applied") or 0),
"layers_extracted": int(coverage.get("layers_extracted") or 0),
"materials_extracted": int(coverage.get("materials_extracted") or 0),
"properties_extracted": int(coverage.get("properties_extracted") or 0),
"configuration_record_count": len(configuration_records),
"configuration_record_types": dict(sorted(configuration_types.items())),
"configuration_metadata_count": len(
report.get("configuration_metadata") or {}
),
"product_alternative_count": len(
report.get("product_alternatives") or []
),
"usage_substitute_count": len(report.get("usage_substitutes") or []),
"entity_type_counts": dict(sorted(
(coverage.get("entity_counts") or {}).items()
)),
"unsupported_entity_counts": dict(sorted(
(coverage.get("unsupported_counts") or {}).items()
)),
"representation_status_counts": dict(sorted(
representation_statuses.items()
)),
"clean": status["process_exit_status"] not in
INCOMPLETE_PROCESS_STATUSES and attempted > 0 and
written == attempted and
converter_status == 0 and
invalid_breps_written == 0 and output_failures == 0 and
inferred_curves == 0,
"intentionally_empty": intentionally_empty,
"invalid_breps": invalid_breps,
"invalid_breps_written": invalid_breps_written,
"invalid_breps_rejected": invalid_breps_rejected,
"output_failures": output_failures,
"repairs": int(validation.get("repairs") or 0),
"inferred_curves": inferred_curves,
"tolerance_mm": validation.get("tolerance_mm"),
"peak_rss_bytes": int(report.get("peak_rss_bytes") or 0),
"load_us": int(timings.get("load") or 0),
"index_us": int(timings.get("index") or 0),
"inventory_us": int(timings.get("inventory") or 0),
"convert_us": int(timings.get("convert") or 0),
"calibration": {
"queries": int(calibration.get("queries") or 0),
"elapsed_us": int(calibration.get("elapsed_us") or 0),
"scalar_queries_per_second": float(
calibration.get("scalar_queries_per_second") or 0.0
),
"parallel_queries_per_second": float(
calibration.get("parallel_queries_per_second") or 0.0
),
},
"stage_timings_us": {
name: int(values.get("total_us") or 0)
for name, values in sorted(stage_timings.items())
},
"failure_categories": dict(sorted(categories.items())),
"skipped_items": [
{
"entity_id": item.get("entity_id"),
"entity_type": item.get("entity_type"),
"reason": item.get("reason"),
"category": failure_category(item.get("reason")),
}
for item in skipped
],
}
return result
def totals(files):
by_schema = {}
for schema in sorted(set(item["schema"] for item in files)):
by_schema[schema] = totals_base(
[item for item in files if item["schema"] == schema]
)
result = totals_base(files)
result["by_schema"] = by_schema
return result
def totals_base(files):
completed = [item for item in files if run_complete(item)]
incomplete = [item for item in files if not run_complete(item)]
process_statuses = Counter(
str(item["process_exit_status"]) for item in completed
)
converter_statuses = Counter(
str(item["converter_exit_status"]) for item in completed
)
attempted = sum(item["geometry_attempted"] for item in completed)
written = sum(item["geometry_written"] for item in completed)
categories = Counter()
entity_types = Counter()
unsupported_types = Counter()
configuration_types = Counter()
representation_statuses = Counter()
occurrence_methods = set()
for item in completed:
categories.update(item["failure_categories"])
entity_types.update(item.get("entity_type_counts") or {})
unsupported_types.update(item.get("unsupported_entity_counts") or {})
configuration_types.update(item.get("configuration_record_types") or {})
representation_statuses.update(
item.get("representation_status_counts") or {}
)
occurrence_methods.update(item.get("occurrence_methods") or [])
return {
"files": len(files),
"completed_files": len(completed),
"inconclusive_files": len(incomplete),
"clean_files": sum(1 for item in completed if item["clean"]),
"intentionally_empty_files": sum(
1 for item in completed if item.get("intentionally_empty")
),
"process_exit_status_counts": dict(sorted(process_statuses.items())),
"converter_exit_status_counts": dict(sorted(converter_statuses.items())),
"geometry_attempted": attempted,
"geometry_written": written,
"geometry_skipped": sum(item["geometry_skipped"] for item in completed),
"success_fraction": (float(written) / attempted) if attempted else 0.0,
"products": sum(item.get("products", 0) for item in completed),
"occurrences": sum(item.get("occurrences", 0) for item in completed),
"assembly_usage_count": sum(
item.get("assembly_usage_count", 0) for item in completed
),
"occurrence_detail_count": sum(
item.get("occurrence_detail_count", 0) for item in completed
),
"occurrence_max_depth": max(
(item.get("occurrence_max_depth", 0) for item in completed),
default=0,
),
"occurrence_methods": sorted(occurrence_methods),
"styles_extracted": sum(
item.get("styles_extracted", 0) for item in completed
),
"styles_applied": sum(
item.get("styles_applied", 0) for item in completed
),
"layers_extracted": sum(
item.get("layers_extracted", 0) for item in completed
),
"materials_extracted": sum(
item.get("materials_extracted", 0) for item in completed
),
"properties_extracted": sum(
item.get("properties_extracted", 0) for item in completed
),
"configuration_record_count": sum(
item.get("configuration_record_count", 0) for item in completed
),
"configuration_metadata_count": sum(
item.get("configuration_metadata_count", 0) for item in completed
),
"product_alternative_count": sum(
item.get("product_alternative_count", 0) for item in completed
),
"usage_substitute_count": sum(
item.get("usage_substitute_count", 0) for item in completed
),
"invalid_breps": sum(
item.get("invalid_breps", 0) for item in completed
),
"invalid_breps_written": sum(
item.get("invalid_breps_written", 0) for item in completed
),
"invalid_breps_rejected": sum(
item.get("invalid_breps_rejected", 0) for item in completed
),
"output_failures": sum(
item.get("output_failures", 0) for item in completed
),
"repairs": sum(item.get("repairs", 0) for item in completed),
"inferred_curves": sum(
item.get("inferred_curves", 0) for item in completed
),
"elapsed_seconds": sum(item["elapsed_seconds"] for item in completed),
"load_seconds": sum(item.get("load_us", 0) for item in completed) / 1.0e6,
"index_seconds": sum(item.get("index_us", 0) for item in completed) / 1.0e6,
"inventory_seconds": sum(
item.get("inventory_us", 0) for item in completed
) / 1.0e6,
"convert_seconds": sum(
item.get("convert_us", 0) for item in completed
) / 1.0e6,
"inconclusive_elapsed_seconds": sum(
item["elapsed_seconds"] for item in incomplete
),
"observed_inconclusive_geometry_attempted": sum(
item["geometry_attempted"] for item in incomplete
),
"observed_inconclusive_geometry_written": sum(
item["geometry_written"] for item in incomplete
),
"observed_inconclusive_geometry_skipped": sum(
item["geometry_skipped"] for item in incomplete
),
"peak_file_rss_bytes": max(
(item["peak_rss_bytes"] for item in files), default=0
),
"entity_type_counts": dict(sorted(entity_types.items())),
"unsupported_entity_counts": dict(sorted(unsupported_types.items())),
"configuration_record_types": dict(sorted(configuration_types.items())),
"representation_status_counts": dict(
sorted(representation_statuses.items())
),
"failure_categories": dict(sorted(categories.items())),
}
def positive_ratio(current, previous):
return (float(current) / float(previous)) if previous > 0 else None
def median_ratio(current_files, previous_files, field):
ratios = [
positive_ratio(current.get(field, 0), previous.get(field, 0))
for current, previous in zip(current_files, previous_files)
]
ratios = [value for value in ratios if value is not None and value > 0]
return statistics.median(ratios) if ratios else None
def geometric_mean_ratio(current_files, previous_files, field):
ratios = [
positive_ratio(current.get(field, 0), previous.get(field, 0))
for current, previous in zip(current_files, previous_files)
]
ratios = [value for value in ratios if value is not None and value > 0]
return math.exp(sum(math.log(value) for value in ratios) / len(ratios)) \
if ratios else None
def performance_comparison(current_files, previous_files):
current = totals_base(current_files)
previous = totals_base(previous_files)
fields = {
"elapsed": "elapsed_seconds",
"load": "load_seconds",
"convert": "convert_seconds",
"peak_file_rss": "peak_file_rss_bytes",
}
aggregate_ratios = {
label: positive_ratio(current[field], previous[field])
for label, field in fields.items()
}
per_file_fields = {
"elapsed": "elapsed_seconds",
"load": "load_us",
"convert": "convert_us",
"peak_rss": "peak_rss_bytes",
}
return {
"comparable_files": len(current_files),
"current": {field: current[field] for field in fields.values()},
"previous": {field: previous[field] for field in fields.values()},
"aggregate_ratios": aggregate_ratios,
"median_file_ratios": {
label: median_ratio(current_files, previous_files, field)
for label, field in per_file_fields.items()
},
"geometric_mean_file_ratios": {
label: geometric_mean_ratio(current_files, previous_files, field)
for label, field in per_file_fields.items()
},
}
def converter_options(configuration):
options = (configuration or {}).get("options") or {}
return {
"jobs": options.get("jobs"),
"exact": bool(options.get("exact", False)),
"stall_timeout_seconds": options.get("stall_timeout_seconds"),
}
def converter_identity(configuration):
configuration = configuration or {}
executable = configuration.get("step_g") or {}
plugins = configuration.get("schema_plugins") or []
libraries = configuration.get("linked_libraries") or []
return {
"host": executable.get("sha256"),
"schema_plugins": [
(os.path.basename(item.get("path") or ""), item.get("sha256"))
for item in plugins
],
"linked_libraries": [
(os.path.basename(item.get("path") or ""), item.get("sha256"))
for item in libraries
],
}
def host_identity(configuration):
host = (configuration or {}).get("host") or {}
return {
key: host.get(key)
for key in ("hostname", "platform", "machine", "cpu_model",
"logical_cpus")
}
def host_observations(manifest):
snapshots = []
host = manifest.get("host") or {}
for name in ("initial_snapshot", "final_snapshot"):
if isinstance(host.get(name), dict):
snapshots.append(host[name])
for item in manifest.get("files", []):
for name in ("host_before", "host_after"):
if isinstance(item.get(name), dict):
snapshots.append(item[name])
loads = [item.get("load_average") for item in snapshots]
loads = [item for item in loads if isinstance(item, list) and len(item) == 3]
available = [
(item.get("memory_bytes") or {}).get("MemAvailable")
for item in snapshots
]
available = [int(value) for value in available if value is not None]
result = {"snapshot_count": len(snapshots)}
if loads:
labels = ("one_minute", "five_minutes", "fifteen_minutes")
result["load_average"] = {
label: {
"minimum": min(float(item[index]) for item in loads),
"mean": statistics.mean(float(item[index]) for item in loads),
"maximum": max(float(item[index]) for item in loads),
}
for index, label in enumerate(labels)
}
if available:
result["available_memory_bytes"] = {
"minimum": min(available),
"maximum": max(available),
}
return result
def compare(current_files, previous, current_configuration=None,
previous_configuration=None):
if not previous:
return None
old_files = {item["input"]: item for item in previous.get("files", [])}
changes = []
comparable_current = []
comparable_previous = []
for current in current_files:
old = old_files.get(current["input"])
if not run_complete(current):
changes.append({
"input": current["input"],
"change": "inconclusive",
"process_exit_status": current["process_exit_status"],
})
continue
if not old:
changes.append({"input": current["input"], "change": "new"})
continue
if not run_complete(old):
changes.append({"input": current["input"],
"change": "previous_inconclusive"})
continue
comparable_current.append(current)
comparable_previous.append(old)
written_delta = current["geometry_written"] - old.get("geometry_written", 0)
skipped_delta = current["geometry_skipped"] - old.get("geometry_skipped", 0)
inferred_delta = current.get("inferred_curves", 0) - \
old.get("inferred_curves", 0)
old_skips = {
(item.get("entity_id"), item.get("entity_type"), item.get("reason"))
for item in old.get("skipped_items", [])
}
new_skips = {
(item.get("entity_id"), item.get("entity_type"), item.get("reason"))
for item in current.get("skipped_items", [])
}
if (written_delta or skipped_delta or inferred_delta or
bool(old.get("clean")) != current["clean"] or
old_skips != new_skips):
changes.append({
"input": current["input"],
"written_delta": written_delta,
"skipped_delta": skipped_delta,
"inferred_curves_delta": inferred_delta,
"became_clean": current["clean"] and not old.get("clean"),
"regressed_from_clean": bool(old.get("clean")) and not current["clean"],
"fixed_skips": [list(item) for item in sorted(old_skips - new_skips)],
"new_skips": [list(item) for item in sorted(new_skips - old_skips)],
})
previous_totals = totals(comparable_previous)
current_totals = totals(comparable_current)
input_hash_pairs = [
(current.get("input_sha256"), old.get("input_sha256"))
for current, old in zip(comparable_current, comparable_previous)
]
input_hashes_verified = bool(input_hash_pairs) and all(
current and old and current == old
for current, old in input_hash_pairs
)
options_match = bool(
current_configuration and previous_configuration
) and converter_options(current_configuration) == converter_options(
previous_configuration
)
hosts_match = bool(
current_configuration and previous_configuration
) and host_identity(current_configuration) == host_identity(
previous_configuration
)
current_observations = (
current_configuration or {}
).get("host_observations") or {}
previous_observations = (
previous_configuration or {}
).get("host_observations") or {}
loads_recorded = bool(
current_observations.get("load_average") and
previous_observations.get("load_average")
)
return {
"previous_run_id": previous.get("run_id"),
"geometry_written_delta": current_totals["geometry_written"] -
int(previous_totals.get("geometry_written") or 0),
"geometry_skipped_delta": current_totals["geometry_skipped"] -
int(previous_totals.get("geometry_skipped") or 0),
"clean_files_delta": current_totals["clean_files"] -
int(previous_totals.get("clean_files") or 0),
"regressed_clean_files": [
item["input"] for item in changes if item.get("regressed_from_clean")
],
"newly_clean_files": [
item["input"] for item in changes if item.get("became_clean")
],
"inconclusive_files": [
item["input"] for item in changes
if item.get("change") == "inconclusive"
],
"configuration": {
"current_options": (current_configuration or {}).get("options"),
"previous_options": (previous_configuration or {}).get("options"),
"converter_options_match": options_match,
"host_identity_match": hosts_match,
"input_hashes_verified": input_hashes_verified,
"load_observations_recorded": loads_recorded,
"timing_context_comparable": options_match and hosts_match and
input_hashes_verified and
loads_recorded,
"executable_match": bool(
(current_configuration or {}).get("step_g") and
(previous_configuration or {}).get("step_g")
) and converter_identity(current_configuration) ==
converter_identity(previous_configuration),
"current_outer_timeout_seconds": (
(current_configuration or {}).get("options") or {}
).get("timeout_seconds"),
"previous_outer_timeout_seconds": (
(previous_configuration or {}).get("options") or {}
).get("timeout_seconds"),
"current_host_identity": host_identity(current_configuration),
"previous_host_identity": host_identity(previous_configuration),
"current_host_observations": current_observations,
"previous_host_observations": previous_observations,
},
"performance": performance_comparison(
comparable_current, comparable_previous
),
"changes": changes,
}
def run_configuration(run_directory):
path = Path(run_directory) / "run-manifest.json"
if not path.is_file():
return None
manifest = read_json(path)
return {
"manifest": str(path.resolve()),
"options": manifest.get("options"),
"step_g": manifest.get("step_g"),
"schema_plugins": manifest.get("schema_plugins"),
"linked_libraries": manifest.get("linked_libraries"),
"host": manifest.get("host"),
"host_observations": host_observations(manifest),
"started_utc": manifest.get("started_utc"),
"finished_utc": manifest.get("finished_utc"),
}
def attach_input_identities(files, run_directory):
path = Path(run_directory) / "run-manifest.json"
if not path.is_file():
return
manifest = read_json(path)
identities = {
record.get("input"): record.get("input_identity") or {}
for record in manifest.get("files") or []
}
for item in files:
identity = identities.get(item["input"]) or {}
if identity.get("sha256"):
item["input_sha256"] = identity["sha256"]
item["input_size"] = identity.get("size")
def markdown(summary):
total = summary["totals"]
def count_list(values):
return ", ".join(
"{} {}".format(count, name)
for name, count in sorted(
values.items(), key=lambda item: (-item[1], item[0])
)
)
lines = [
"# STEP corpus run {}".format(summary["run_id"]),
"",
"Revision: `{}`{}".format(
summary["revision"], " (dirty)" if summary.get("dirty") else ""
),
"",
"| Schema | Files clean | Intentional empty | Geometry written | Success |",
"|---|---:|---:|---:|---:|",
]
for schema, values in sorted(total["by_schema"].items()):
lines.append("| {} | {}/{} | {} | {}/{} | {:.2%} |".format(
schema, values["clean_files"], values["completed_files"],
values["intentionally_empty_files"],
values["geometry_written"], values["geometry_attempted"],
values["success_fraction"],
))
lines.append("| Combined | {}/{} | {} | {}/{} | {:.2%} |".format(
total["clean_files"], total["completed_files"],
total["intentionally_empty_files"], total["geometry_written"],
total["geometry_attempted"], total["success_fraction"],
))
lines.extend([
"",
"Assembly coverage: {} product(s), {} occurrence(s), maximum depth "
"{}, using {}.".format(
total["products"], total["occurrences"],
total["occurrence_max_depth"],
", ".join(total["occurrence_methods"]) or "no occurrence method",
),
"Presentation and metadata: {} style(s) extracted, {} applied, {} "
"layer(s), {} material(s), {} property value(s), and {} retained "
"configuration record(s).".format(
total["styles_extracted"], total["styles_applied"],
total["layers_extracted"], total["materials_extracted"],
total["properties_extracted"],
total["configuration_record_count"],
),
"Validation: {} invalid B-Rep(s), {} preserved and {} rejected; {} "
"bounded repair(s) and {} tagged curve inference(s) applied.".format(
total["invalid_breps"], total["invalid_breps_written"],
total["invalid_breps_rejected"],
total["repairs"],
total["inferred_curves"],
),
])
if total["failure_categories"]:
lines.append("Geometry omission categories: {}.".format(
count_list(total["failure_categories"])
))
lines.append("Converter terminal statuses: {}.".format(
count_list(total["converter_exit_status_counts"])
))
if total["unsupported_entity_counts"]:
lines.append("Unsupported STEP entity inventory: {}.".format(
count_list(total["unsupported_entity_counts"])
))
observations = ((summary.get("run_configuration") or {}).get(
"host_observations") or {})
one_minute = (observations.get("load_average") or {}).get("one_minute")
if one_minute:
lines.extend([
"",
"Host one-minute load over {} boundary samples: {:.2f} mean "
"({:.2f}-{:.2f}).".format(
observations["snapshot_count"], one_minute["mean"],
one_minute["minimum"], one_minute["maximum"],
),
])
if total["inconclusive_files"]:
lines.append("")
lines.append(
"Inconclusive (timed out or interrupted): {} file(s); partial "
"geometry is excluded from totals and deltas.".format(
total["inconclusive_files"]
)
)
lines.extend([
"", "## Per file", "",
"| Schema | Input | Status | Written | Skipped | Inferred | Reported load s | "
"Reported convert s | Wall s | Peak RSS MiB |",
"|---|---|---|---:|---:|---:|---:|---:|---:|---:|",
])
for item in summary["files"]:
lines.append("| {} | {} | {} | {}/{} | {} | {} | {:.1f} | {:.1f} | {:.1f} | {:.1f} |".format(
item["schema"], item["name"].replace("|", "\\|"),
"clean" if item["clean"] else
("intentional empty" if item.get("intentionally_empty") else
("partial" if run_complete(item) else "inconclusive")),
item["geometry_written"], item["geometry_attempted"],
item["geometry_skipped"], item.get("inferred_curves", 0),
item["load_us"] / 1.0e6,
item["convert_us"] / 1.0e6, item["elapsed_seconds"],
item["peak_rss_bytes"] / (1024.0 * 1024.0),
))
delta = summary.get("delta")
if delta:
lines.extend([
"", "## Delta", "",
"Written geometry: {:+d}; skipped geometry: {:+d}; clean files: {:+d}.".format(
delta["geometry_written_delta"], delta["geometry_skipped_delta"],
delta["clean_files_delta"],
),
])
if delta["regressed_clean_files"]:
lines.append("Regressed clean files: {}.".format(
", ".join(delta["regressed_clean_files"])
))
if delta["inconclusive_files"]:
lines.append("Not compared because the run was incomplete: {}.".format(
", ".join(delta["inconclusive_files"])
))
performance = delta.get("performance") or {}
ratios = performance.get("aggregate_ratios") or {}
if performance.get("comparable_files"):
lines.extend([
"",
"Performance over {} comparable files (current/previous): "
"wall {:.3f}x and peak-file RSS {:.3f}x. Reported phase "
"ratios are load {:.3f}x and conversion {:.3f}x; phase "
"boundaries may differ across binaries.".format(
performance["comparable_files"],
ratios.get("elapsed") or 0.0,
ratios.get("peak_file_rss") or 0.0,
ratios.get("load") or 0.0,
ratios.get("convert") or 0.0,
),
])
configuration = delta.get("configuration") or {}
if not configuration.get("converter_options_match"):
lines.append(
"Run options differ or could not be verified; performance "
"ratios are informational only."
)
if not configuration.get("host_identity_match"):
lines.append(
"Host identity differs or could not be verified; timing "
"ratios are not controlled benchmarks."
)
if not configuration.get("input_hashes_verified"):
lines.append(
"Input hashes differ or were not recorded in both runs; "
"timing ratios are informational only."
)
if not configuration.get("load_observations_recorded"):
lines.append(
"Both runs did not record host-load observations; "
"timing ratios are not controlled benchmarks."
)
else:
current_load = configuration["current_host_observations"][
"load_average"
]["one_minute"]["mean"]
previous_load = configuration["previous_host_observations"][
"load_average"
]["one_minute"]["mean"]
lines.append(
"Recorded one-minute load mean was {:.2f} current versus "
"{:.2f} previous.".format(current_load, previous_load)
)
if configuration.get("timing_context_comparable"):
lines.append(
"Host, input hashes, converter options, and host-load "
"telemetry are comparable; unrecorded background activity "
"may still affect timings."
)
return "\n".join(lines) + "\n"
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("run_directory", type=Path)
parser.add_argument("--previous", type=Path,
help="previous corpus-summary.json for delta reporting")
parser.add_argument("--output", type=Path,
help="output JSON (default: RUN_DIRECTORY/corpus-summary.json)")
parser.add_argument("--run-id", help="stable run label; defaults to directory name")
parser.add_argument("--no-markdown", action="store_true")
args = parser.parse_args()
run_directory = args.run_directory.resolve()
status_paths = sorted(run_directory.glob("*.status"))
if not status_paths:
parser.error("{} contains no *.status corpus records".format(run_directory))
try:
files = [report_record(path) for path in status_paths]
except (OSError, ValueError, json.JSONDecodeError) as error:
print("step_corpus_summary.py: {}".format(error), file=sys.stderr)
return 2
attach_input_identities(files, run_directory)
revision, dirty = git_state(Path(__file__).resolve().parents[2])
previous = read_json(args.previous) if args.previous else None
current_configuration = run_configuration(run_directory)
previous_configuration = run_configuration(
previous.get("run_directory")
) if previous and previous.get("run_directory") else None
generated = datetime.datetime.now(datetime.timezone.utc).replace(
microsecond=0
).isoformat()
summary = {
"format": FORMAT,
"run_id": args.run_id or run_directory.name,
"generated_utc": generated,
"run_directory": str(run_directory),
"revision": revision,
"dirty": dirty,
"run_configuration": current_configuration,
"totals": totals(files),
"files": files,
}
summary["delta"] = compare(
files, previous, current_configuration, previous_configuration
)
output = args.output or (run_directory / "corpus-summary.json")
output.parent.mkdir(parents=True, exist_ok=True)
with output.open("w", encoding="utf-8") as stream:
json.dump(summary, stream, indent=2, sort_keys=True)
stream.write("\n")
if not args.no_markdown:
output.with_suffix(".md").write_text(markdown(summary), encoding="utf-8")
return 0
if __name__ == "__main__":
sys.exit(main())
-288
View File
@@ -1,288 +0,0 @@
#!/usr/bin/env python3
"""Verify a recorded STEP corpus run against checksum-pinned expectations.
External STEP samples are often valuable but not suitable for copying into
BRL-CAD. This tool keeps their evidence reproducible without copying the
files: an expectation manifest records the source, accepted local filenames,
SHA-256 digest, and report assertions. The verifier consumes the structured
reports and run manifest produced by ``step_corpus_run.py``.
"""
import argparse
import hashlib
import json
import sys
from pathlib import Path
FORMAT = "brlcad-step-corpus-verification-v1"
def read_json(path):
with path.open("r", encoding="utf-8") as stream:
return json.load(stream)
def file_sha256(path):
digest = hashlib.sha256()
with path.open("rb") as stream:
for block in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def dotted_value(document, path):
value = document
for component in path.split("."):
if not isinstance(value, dict) or component not in value:
raise KeyError(path)
value = value[component]
return value
def occurrence_depth(occurrences):
children = {}
vertices = set()
for occurrence in occurrences:
parent = int(occurrence.get("parent_product_id") or 0)
child = int(occurrence.get("child_product_id") or 0)
if parent <= 0 or child <= 0:
continue
children.setdefault(parent, set()).add(child)
vertices.update((parent, child))
active = set()
memo = {}
def visit(vertex):
if vertex in memo:
return memo[vertex]
if vertex in active:
raise ValueError("occurrence product graph contains a cycle")
active.add(vertex)
depth = max((1 + visit(child) for child in children.get(vertex, ())),
default=0)
active.remove(vertex)
memo[vertex] = depth
return depth
return max((visit(vertex) for vertex in vertices), default=0)
def derived_values(report):
occurrences = report.get("occurrence_details") or []
metadata = report.get("product_metadata") or []
materials = [
material
for product in metadata
for material in product.get("materials") or []
]
material_properties = [
prop
for material in materials
for prop in material.get("properties") or []
]
material_numeric_values = sorted({
value
for prop in material_properties
for value in prop.get("values") or []
if isinstance(value, (int, float))
})
return {
"assembly_usage_count": len(report.get("assembly_usages") or []),
"occurrence_detail_count": len(occurrences),
"occurrence_max_depth": occurrence_depth(occurrences),
"occurrence_methods": sorted({
item.get("shape_method") or "" for item in occurrences
}),
"skipped_entity_ids": sorted({
int(item.get("entity_id") or 0)
for item in report.get("skipped_items") or []
if int(item.get("entity_id") or 0) > 0
}),
"product_metadata_count": len(metadata),
"material_count": len(materials),
"material_identifiers": sorted({
material.get("identifier") or "" for material in materials
}),
"material_names": sorted({
material.get("name") or "" for material in materials
}),
"material_property_count": len(material_properties),
"material_property_names": sorted({
prop.get("name") or "" for prop in material_properties
}),
"material_property_units": sorted({
prop.get("units") or "" for prop in material_properties
if prop.get("units")
}),
"material_property_numeric_values": material_numeric_values,
"validation_property_count": sum(
len(product.get("validation_properties") or [])
for product in metadata
),
}
def compare_expected(report, expected):
combined = dict(report)
combined["derived"] = derived_values(report)
checks = []
errors = []
for path, wanted in sorted((expected.get("equals") or {}).items()):
try:
actual = dotted_value(combined, path)
except KeyError:
errors.append("{} is missing".format(path))
continue
checks.append(path)
if actual != wanted:
errors.append("{} is {!r}, expected {!r}".format(
path, actual, wanted
))
for path, wanted in sorted((expected.get("minimum") or {}).items()):
try:
actual = dotted_value(combined, path)
except KeyError:
errors.append("{} is missing".format(path))
continue
checks.append(path)
if not isinstance(actual, (int, float)) or actual < wanted:
errors.append("{} is {!r}, expected at least {!r}".format(
path, actual, wanted
))
return checks, errors, combined["derived"]
def markdown(result):
lines = [
"# STEP external corpus verification",
"",
"Expectation manifest: `{}`".format(result["expectations"]),
"",
"| Input | Source/producer | Result | Checks |",
"|---|---|---:|---:|",
]
for item in result["files"]:
source = item.get("producer") or item.get("source_page") or ""
lines.append("| {} | {} | {} | {} |".format(
item["name"].replace("|", "\\|"),
source.replace("|", "\\|"),
item["result"], len(item.get("checks") or []),
))
for error in item.get("errors") or []:
lines.append(" - **{}:** {}".format(item["name"], error))
lines.extend([
"",
"Verified {}/{} present files; {} manifest entries were missing.".format(
result["passed_files"], result["present_files"],
result["missing_files"],
),
])
return "\n".join(lines) + "\n"
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("run_directory", type=Path)
parser.add_argument("expectations", type=Path)
parser.add_argument("--allow-missing", action="store_true",
help="do not fail for expectation entries absent from the run")
parser.add_argument("--output", type=Path,
help="output JSON (default: RUN/corpus-verification.json)")
args = parser.parse_args()
run_directory = args.run_directory.resolve()
manifest_path = run_directory / "run-manifest.json"
if not manifest_path.is_file():
parser.error("{} does not contain run-manifest.json".format(run_directory))
expectations_path = args.expectations.resolve()
run = read_json(manifest_path)
expectations = read_json(expectations_path)
if expectations.get("format") != "brlcad-step-corpus-expectations-v1":
parser.error("unsupported expectation manifest format")
run_files = {}
for record in run.get("files") or []:
input_name = Path(record.get("input") or "").name
if input_name:
run_files[input_name] = record
results = []
passed = 0
present = 0
missing = 0
for expectation in expectations.get("files") or []:
names = [expectation["name"]] + list(expectation.get("aliases") or [])
record = next((run_files[name] for name in names if name in run_files), None)
item = {
"name": expectation["name"],
"source_page": expectation.get("source_page"),
"producer": expectation.get("producer"),
"checks": [],
"errors": [],
}
if record is None:
missing += 1
item["result"] = "missing"
if not args.allow_missing:
item["errors"].append("input was not present in the corpus run")
results.append(item)
continue
present += 1
input_path = Path(record["input"])
actual_hash = file_sha256(input_path) if input_path.is_file() else None
item["input"] = str(input_path)
item["sha256"] = actual_hash
item["checks"].append("sha256")
recorded_hash = (record.get("input_identity") or {}).get("sha256")
if recorded_hash and recorded_hash != actual_hash:
item["errors"].append(
"input changed after the run manifest recorded its SHA-256"
)
if actual_hash != expectation.get("sha256"):
item["errors"].append("SHA-256 is {}, expected {}".format(
actual_hash, expectation.get("sha256")
))
report_name = record.get("report")
report_path = run_directory / report_name if report_name else None
if not report_path or not report_path.is_file():
item["errors"].append("structured converter report is missing")
else:
report = read_json(report_path)
checks, errors, derived = compare_expected(
report, expectation.get("expected") or {}
)
item["checks"].extend(checks)
item["errors"].extend(errors)
item["derived"] = derived
item["report"] = str(report_path)
item["result"] = "pass" if not item["errors"] else "fail"
if item["result"] == "pass":
passed += 1
results.append(item)
failed = sum(1 for item in results if item.get("errors"))
result = {
"format": FORMAT,
"expectations": str(expectations_path),
"run_directory": str(run_directory),
"passed_files": passed,
"present_files": present,
"missing_files": missing,
"failed_files": failed,
"files": results,
}
output = args.output or (run_directory / "corpus-verification.json")
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n",
encoding="utf-8")
output.with_suffix(".md").write_text(markdown(result), encoding="utf-8")
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main())
-449
View File
@@ -1,449 +0,0 @@
#!/usr/bin/env python3
"""Extract a small, forward-complete STEP geometry subset.
This helper is intended for local diagnosis and for identifying the minimal
geometry that a project-owned synthetic regression test must reproduce. It
copies the dependency closure of one or more STEP face entities and wraps them
in an OPEN_SHELL product, retaining the source entity numbers so converter
diagnostics remain directly comparable to the original file. A diagnostic
--closed-shell mode instead uses CLOSED_SHELL/MANIFOLD_SOLID_BREP so reductions
can exercise topology rules which only apply to asserted solids. A face subset
is not necessarily a valid closed shell, so that mode is for failure reduction,
not evidence that the extracted result is valid geometry. The output keeps the
input model's provenance and is not automatically suitable for committing or
redistribution.
"""
import argparse
import re
from pathlib import Path
ADVANCED_FACE = re.compile(
r"^[ \t]*#[0-9]+[ \t]*=[ \t]*ADVANCED_FACE[ \t]*\(",
re.IGNORECASE,
)
FILE_SCHEMA = re.compile(
r"FILE_SCHEMA\s*\(\s*\(\s*'([^']+)'", re.IGNORECASE | re.DOTALL
)
ENTITY_TYPE = re.compile(
r"^[ \t]*#[0-9]+[ \t]*=[ \t]*([A-Z0-9_]+)[ \t]*\(",
re.IGNORECASE,
)
def instance_end(text, start):
"""Return the first record-ending semicolon outside strings/comments."""
in_string = False
in_comment = False
offset = start
while offset < len(text):
current = text[offset]
following = text[offset + 1] if offset + 1 < len(text) else ""
if in_comment:
if current == "*" and following == "/":
in_comment = False
offset += 2
continue
elif in_string:
if current == "'":
if following == "'":
offset += 2
continue
in_string = False
elif current == "/" and following == "*":
in_comment = True
offset += 2
continue
elif current == "'":
in_string = True
elif current == ";":
return offset + 1
offset += 1
raise ValueError("unterminated STEP instance starting at byte {}".format(start))
def instances(text):
"""Return Part 21 instances without recognizing text in strings/comments."""
result = {}
offset = 0
line_start = True
in_string = False
in_comment = False
while offset < len(text):
current = text[offset]
following = text[offset + 1] if offset + 1 < len(text) else ""
if in_comment:
if current == "*" and following == "/":
in_comment = False
offset += 2
continue
elif in_string:
if current == "'":
if following == "'":
offset += 2
continue
in_string = False
elif current == "/" and following == "*":
in_comment = True
offset += 2
continue
elif current == "'":
in_string = True
elif current == "\n" or current == "\r":
line_start = True
elif line_start and current in " \t":
pass
elif line_start and current == "#":
digit_end = offset + 1
while digit_end < len(text) and text[digit_end].isdigit():
digit_end += 1
separator = digit_end
while separator < len(text) and text[separator] in " \t":
separator += 1
if digit_end > offset + 1 and separator < len(text) and \
text[separator] == "=":
entity_id = int(text[offset + 1:digit_end])
end = instance_end(text, offset)
if entity_id in result:
raise ValueError(
"duplicate STEP entity #{}".format(entity_id)
)
result[entity_id] = text[offset:end].strip()
offset = end
line_start = False
continue
line_start = False
else:
line_start = False
offset += 1
return result
def references(record):
"""Yield entity references outside Part 21 strings and comments."""
offset = 0
in_string = False
in_comment = False
while offset < len(record):
current = record[offset]
following = record[offset + 1] if offset + 1 < len(record) else ""
if in_comment:
if current == "*" and following == "/":
in_comment = False
offset += 2
continue
elif in_string:
if current == "'":
if following == "'":
offset += 2
continue
in_string = False
elif current == "/" and following == "*":
in_comment = True
offset += 2
continue
elif current == "'":
in_string = True
elif current == "#":
digit_end = offset + 1
while digit_end < len(record) and record[digit_end].isdigit():
digit_end += 1
if digit_end > offset + 1:
yield int(record[offset + 1:digit_end])
offset = digit_end
continue
offset += 1
def dependency_closure(records, roots):
pending = list(roots)
selected = set()
while pending:
entity_id = pending.pop()
if entity_id in selected:
continue
record = records.get(entity_id)
if record is None:
raise ValueError("STEP entity #{} is not present".format(entity_id))
selected.add(entity_id)
for referenced_id in references(record):
if referenced_id != entity_id and referenced_id not in selected:
pending.append(referenced_id)
return selected
def record_type(record):
match = ENTITY_TYPE.match(record)
return match.group(1).upper() if match else ""
def adjacent_faces(records, faces):
"""Return ADVANCED_FACE entities sharing an EDGE_CURVE with faces."""
reverse = {}
for entity_id, record in records.items():
for referenced_id in references(record):
reverse.setdefault(referenced_id, set()).add(entity_id)
edge_curves = {
entity_id
for entity_id in dependency_closure(records, faces)
if record_type(records[entity_id]) == "EDGE_CURVE"
}
oriented_edges = {
entity_id
for edge_id in edge_curves
for entity_id in reverse.get(edge_id, ())
if record_type(records[entity_id]) == "ORIENTED_EDGE"
}
edge_loops = {
entity_id
for oriented_id in oriented_edges
for entity_id in reverse.get(oriented_id, ())
if record_type(records[entity_id]) == "EDGE_LOOP"
}
face_bounds = {
entity_id
for loop_id in edge_loops
for entity_id in reverse.get(loop_id, ())
if record_type(records[entity_id]) in {
"FACE_BOUND", "FACE_OUTER_BOUND"
}
}
return {
entity_id
for bound_id in face_bounds
for entity_id in reverse.get(bound_id, ())
if record_type(records[entity_id]) == "ADVANCED_FACE"
}
def quoted(value):
return value.replace("'", "''")
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("input", type=Path)
parser.add_argument("output", type=Path)
parser.add_argument(
"--face",
type=int,
action="append",
default=[],
help="ADVANCED_FACE entity ID to retain (repeatable)",
)
parser.add_argument(
"--face-range",
action="append",
default=[],
metavar="FIRST:LAST",
help="inclusive contiguous ADVANCED_FACE ID range to retain (repeatable)",
)
parser.add_argument(
"--face-neighbor-depth",
type=int,
default=0,
metavar="N",
help=(
"also retain ADVANCED_FACE neighbors reachable through N layers "
"of shared EDGE_CURVEs"
),
)
parser.add_argument("--name", default="reduced_step_regression")
parser.add_argument(
"--context",
type=int,
help=(
"source GEOMETRIC_REPRESENTATION_CONTEXT entity ID to preserve; "
"recommended when source angle or length units are not SI defaults"
),
)
parser.add_argument(
"--uncertainty",
type=float,
default=1.0e-6,
help="fixture uncertainty in millimetres (default: 1e-6)",
)
parser.add_argument(
"--closed-shell",
action="store_true",
help=(
"wrap the face subset as CLOSED_SHELL/MANIFOLD_SOLID_BREP "
"instead of OPEN_SHELL/SHELL_BASED_SURFACE_MODEL"
),
)
args = parser.parse_args()
faces = list(args.face)
for face_range in args.face_range:
fields = face_range.split(":", 1)
if len(fields) != 2:
parser.error("--face-range must be FIRST:LAST")
try:
first, last = (int(field) for field in fields)
except ValueError:
parser.error("--face-range must contain integer entity IDs")
if first > last:
parser.error("--face-range FIRST must not exceed LAST")
faces.extend(range(first, last + 1))
faces = sorted(set(faces))
if not faces:
parser.error("at least one --face or --face-range is required")
if args.face_neighbor_depth < 0:
parser.error("--face-neighbor-depth must be nonnegative")
source = args.input.read_text(encoding="latin-1")
records = instances(source)
for entity_id in faces:
record = records.get(entity_id)
if record is None:
parser.error("STEP entity #{} is not present".format(entity_id))
if not ADVANCED_FACE.match(record):
parser.error(
"STEP entity #{} is not an ADVANCED_FACE".format(entity_id)
)
expanded_faces = set(faces)
frontier = set(faces)
for _ in range(args.face_neighbor_depth):
neighbors = adjacent_faces(records, frontier) - expanded_faces
if not neighbors:
break
expanded_faces.update(neighbors)
frontier = neighbors
faces = sorted(expanded_faces)
closure_roots = list(faces)
if args.context is not None:
closure_roots.append(args.context)
selected = dependency_closure(records, closure_roots)
schema_match = FILE_SCHEMA.search(source)
schema = (
schema_match.group(1)
if schema_match
else "AUTOMOTIVE_DESIGN { 1 0 10303 214 3 1 1 1 }"
)
next_id = max(records) + 1
wrapper_ids = list(range(next_id, next_id + 18))
(
shell_id,
model_id,
length_id,
angle_id,
solid_angle_id,
uncertainty_id,
context_id,
application_id,
protocol_id,
product_context_id,
definition_context_id,
product_id,
formation_id,
definition_id,
definition_shape_id,
representation_id,
shape_definition_id,
_unused_id,
) = wrapper_ids
safe_name = quoted(args.name)
face_refs = ",".join("#{}".format(entity_id) for entity_id in faces)
if args.closed_shell:
wrapper = [
"#{}=CLOSED_SHELL('{} shell',({}));".format(
shell_id, safe_name, face_refs
),
"#{}=MANIFOLD_SOLID_BREP('{} model',#{});".format(
model_id, safe_name, shell_id
),
]
else:
wrapper = [
"#{}=OPEN_SHELL('{} shell',({}));".format(
shell_id, safe_name, face_refs
),
"#{}=SHELL_BASED_SURFACE_MODEL('{} model',(#{}));".format(
model_id, safe_name, shell_id
),
]
if args.context is None:
wrapper.extend(
[
"#{}=(LENGTH_UNIT() NAMED_UNIT(*) "
"SI_UNIT(.MILLI.,.METRE.));".format(length_id),
"#{}=(NAMED_UNIT(*) PLANE_ANGLE_UNIT() "
"SI_UNIT($,.RADIAN.));".format(angle_id),
"#{}=(NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) "
"SOLID_ANGLE_UNIT());".format(solid_angle_id),
"#{}=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE({:.17g}),"
"#{},'distance_accuracy_value','maximum gap value');".format(
uncertainty_id, args.uncertainty, length_id
),
"#{}=(GEOMETRIC_REPRESENTATION_CONTEXT(3) "
"GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#{})) "
"GLOBAL_UNIT_ASSIGNED_CONTEXT((#{},#{},#{})) "
"REPRESENTATION_CONTEXT('fixture','3D'));".format(
context_id,
uncertainty_id,
length_id,
angle_id,
solid_angle_id,
),
]
)
representation_context_id = (
args.context if args.context is not None else context_id
)
wrapper.extend(
[
"#{}=APPLICATION_CONTEXT('automotive design');".format(application_id),
"#{}=APPLICATION_PROTOCOL_DEFINITION("
"'fixture','automotive_design',2010,#{});".format(
protocol_id, application_id
),
"#{}=PRODUCT_CONTEXT('',#{},'mechanical');".format(
product_context_id, application_id
),
"#{}=PRODUCT_DEFINITION_CONTEXT('part definition',#{},'design');".format(
definition_context_id, application_id
),
"#{}=PRODUCT('{}','{}','reduced corpus regression fixture',(#{}));".format(
product_id, safe_name, safe_name, product_context_id
),
"#{}=PRODUCT_DEFINITION_FORMATION_WITH_SPECIFIED_SOURCE("
"'1','',#{},.MADE.);".format(formation_id, product_id),
"#{}=PRODUCT_DEFINITION('design','',#{},#{});".format(
definition_id, formation_id, definition_context_id
),
"#{}=PRODUCT_DEFINITION_SHAPE('','',#{});".format(
definition_shape_id, definition_id
),
"#{}={}('',(#{}),#{});".format(
representation_id,
"ADVANCED_BREP_SHAPE_REPRESENTATION" if args.closed_shell else
"MANIFOLD_SURFACE_SHAPE_REPRESENTATION",
model_id,
representation_context_id,
),
"#{}=SHAPE_DEFINITION_REPRESENTATION(#{},#{});".format(
shape_definition_id, definition_shape_id, representation_id
),
]
)
lines = [
"ISO-10303-21;",
"HEADER;",
"FILE_DESCRIPTION(('BRL-CAD reduced corpus regression fixture'),'2;1');",
"FILE_NAME('{}','',('BRL-CAD'),('BRL-CAD'),'','','');".format(safe_name),
"FILE_SCHEMA(('{}'));".format(quoted(schema)),
"ENDSEC;",
"DATA;",
]
lines.extend(records[entity_id] for entity_id in sorted(selected))
lines.extend(wrapper)
lines.extend(("ENDSEC;", "END-ISO-10303-21;", ""))
args.output.write_text("\n".join(lines), encoding="latin-1")
if __name__ == "__main__":
main()
@@ -1,178 +0,0 @@
{
"format": "brlcad-step-corpus-expectations-v1",
"description": "Checksum-pinned external samples used for independent producer qualification without copying their exchange files into BRL-CAD.",
"redistribution": "The linked STEP Tools pages state All Rights Reserved, while the STEPcode collective work is distributed under its 3-clause BSD COPYING terms. Obtain each sample from its stated source; this manifest does not independently grant redistribution rights.",
"files": [
{
"name": "cubsomcy.stp",
"aliases": ["ap203_cubsomcy.stp"],
"sha256": "eb8fb40f56523fb154b435066f7b2719a88e15e30d0b8d8e6d01dd99339ddcf1",
"source_page": "https://www.steptools.com/docs/stpfiles/ap203/index.html",
"producer": "CATIA / Alcatel Space Industries",
"expected": {
"equals": {
"exit_status": 0,
"coverage.products": 1,
"coverage.occurrences": 0,
"coverage.geometry_attempted": 2,
"coverage.geometry_written": 2,
"coverage.geometry_skipped": 0,
"coverage.outcome": "complete",
"coverage.entity_counts.ADVANCED_BREP_SHAPE_REPRESENTATION": 1,
"coverage.entity_counts.FACETED_BREP_SHAPE_REPRESENTATION": 1,
"derived.occurrence_max_depth": 0,
"derived.skipped_entity_ids": []
}
}
},
{
"name": "moon_buggy_asm.stp",
"aliases": ["ap203_moon_buggy_asm.stp"],
"sha256": "2afaaf50cd7ec15e36367be0a1e41730e7af5440ed974b62a0a21cf6346289dd",
"source_page": "https://www.steptools.com/docs/stpfiles/ap203/index.html",
"producer": "STEPNet / EDS Unigraphics 10.5",
"expected": {
"equals": {
"exit_status": 1,
"coverage.products": 20,
"coverage.occurrences": 23,
"coverage.geometry_attempted": 12,
"coverage.geometry_written": 9,
"coverage.geometry_skipped": 3,
"coverage.outcome": "partial",
"coverage.entity_counts.MAPPED_ITEM": 23,
"derived.assembly_usage_count": 23,
"derived.occurrence_detail_count": 23,
"derived.occurrence_max_depth": 4,
"derived.occurrence_methods": ["mapped"],
"derived.skipped_entity_ids": [32060, 32220, 32820]
}
}
},
{
"name": "123Block_Color.stp",
"aliases": ["ap203e2_123Block_Color.stp"],
"sha256": "4bbdd752fd3c4e7ebfee633da210b9e3df62c583a604e874ca5e9c36545d2be4",
"source_page": "https://www.steptools.com/docs/stpfiles/ap203e2/index.html",
"producer": "CATIA / Boeing",
"expected": {
"equals": {
"exit_status": 0,
"coverage.products": 1,
"coverage.geometry_attempted": 1,
"coverage.geometry_written": 1,
"coverage.geometry_skipped": 0,
"coverage.outcome": "complete",
"coverage.styles_extracted": 1,
"coverage.styles_applied": 1,
"derived.skipped_entity_ids": []
}
}
},
{
"name": "boxy_with_linearsize.stp",
"aliases": ["ap203e2_boxy_with_linearsize.stp"],
"sha256": "e120b982c5bb5c8b62c99bb03d5ae46c16d9200fcb53be6521c201be98ead6d6",
"source_page": "https://www.steptools.com/docs/stpfiles/ap214/index.html",
"producer": "STEP-NC Machine / STEP Tools, Inc.",
"expected": {
"equals": {
"exit_status": 3,
"coverage.products": 1,
"coverage.geometry_attempted": 1,
"coverage.geometry_written": 0,
"coverage.geometry_skipped": 1,
"coverage.outcome": "failed",
"coverage.entity_counts.DATUM": 3,
"coverage.entity_counts.PLUS_MINUS_TOLERANCE": 1,
"derived.skipped_entity_ids": [26]
}
}
},
{
"name": "as1-ac-214.stp",
"aliases": ["ap214_as1-ac-214.stp"],
"sha256": "1bb1a0e55dc4a0169e329529eb520596690f8745077c1bdd2f9fa41fa5a2aa95",
"source_page": "https://www.steptools.com/docs/stpfiles/ap214/index.html",
"producer": "AutoCAD / STEPnet",
"expected": {
"equals": {
"exit_status": 0,
"coverage.products": 9,
"coverage.occurrences": 13,
"coverage.geometry_attempted": 5,
"coverage.geometry_written": 5,
"coverage.geometry_skipped": 0,
"coverage.outcome": "complete",
"coverage.styles_extracted": 5,
"coverage.styles_applied": 5,
"coverage.layers_extracted": 1,
"derived.assembly_usage_count": 13,
"derived.occurrence_detail_count": 13,
"derived.occurrence_max_depth": 3,
"derived.occurrence_methods": ["referenced"],
"derived.skipped_entity_ids": []
}
}
},
{
"name": "as1-ug-214.stp",
"aliases": ["ap214_as1-ug-214.stp"],
"sha256": "53c5069f5d6b59435eb32ac743cc82e53bd3607954c94785d989d579d0e29426",
"source_page": "https://www.steptools.com/docs/stpfiles/ap214/index.html",
"producer": "Unigraphics / STEPnet",
"expected": {
"equals": {
"exit_status": 0,
"coverage.products": 9,
"coverage.occurrences": 13,
"coverage.geometry_attempted": 5,
"coverage.geometry_written": 5,
"coverage.geometry_skipped": 0,
"coverage.outcome": "complete",
"coverage.styles_extracted": 5,
"coverage.styles_applied": 5,
"coverage.layers_extracted": 3,
"coverage.properties_extracted": 42,
"derived.assembly_usage_count": 13,
"derived.occurrence_detail_count": 13,
"derived.occurrence_max_depth": 3,
"derived.occurrence_methods": ["referenced"],
"derived.validation_property_count": 42,
"derived.skipped_entity_ids": []
}
}
},
{
"name": "dm1-id-214.stp",
"sha256": "25e727cdc4d738064da7907d604bd4ceeb4e941c6fdc201f3df04c0b94671661",
"source_page": "STEPcode data/ap214e3/dm1-id-214.stp (3-clause BSD collective work)",
"producer": "I-DEAS Master Series 9 / SDRC",
"expected": {
"equals": {
"exit_status": 0,
"coverage.products": 7,
"coverage.occurrences": 7,
"coverage.geometry_attempted": 3,
"coverage.geometry_written": 3,
"coverage.geometry_skipped": 0,
"coverage.outcome": "complete",
"coverage.styles_extracted": 3,
"coverage.styles_applied": 3,
"coverage.materials_extracted": 3,
"coverage.properties_extracted": 22,
"coverage.entity_counts.MAKE_FROM_USAGE_OPTION": 3,
"derived.assembly_usage_count": 7,
"derived.occurrence_detail_count": 7,
"derived.occurrence_max_depth": 1,
"derived.occurrence_methods": ["referenced"],
"derived.material_count": 3,
"derived.material_identifiers": ["AMS 4928", "AMS 5613", "AMS 5662"],
"derived.material_names": ["Greek Ascoloy", "Inconel 718", "Titanium 6-4"],
"derived.validation_property_count": 19,
"derived.skipped_entity_ids": []
}
}
}
]
}
@@ -1,46 +0,0 @@
{
"format": "brlcad-step-corpus-expectations-v1",
"description": "Checksum-pinned independent-producer samples from locally retained, non-redistributed corpora whose original download pages were not recorded.",
"redistribution": "These records provide reproducible local qualification evidence only. They do not grant permission to copy the exchange files into BRL-CAD or redistribute them.",
"files": [
{
"name": "Leopard2-A6.step",
"sha256": "ef8f21f6454f7d952b619ed1e41dab824f31f393e81a3e138392c53605a0ab94",
"source_page": "Local non-redistributed GrabCAD corpus; original download page not recorded",
"producer": "Autodesk Inventor 2018 / ST-Developer v17",
"expected": {
"equals": {
"exit_status": 1,
"coverage.products": 14,
"coverage.occurrences": 338,
"coverage.geometry_attempted": 31,
"coverage.geometry_written": 31,
"coverage.geometry_skipped": 0,
"coverage.outcome": "partial",
"coverage.styles_extracted": 2425,
"coverage.styles_applied": 31,
"coverage.materials_extracted": 13,
"coverage.properties_extracted": 26,
"coverage.properties_invalid": 13,
"validation.invalid_breps": 1,
"validation.invalid_breps_written": 1,
"validation.invalid_breps_rejected": 0,
"derived.assembly_usage_count": 338,
"derived.occurrence_detail_count": 338,
"derived.occurrence_max_depth": 1,
"derived.occurrence_methods": ["referenced"],
"derived.product_metadata_count": 13,
"derived.material_count": 13,
"derived.material_identifiers": [""],
"derived.material_names": ["Generic", "SAE 1020"],
"derived.material_property_count": 26,
"derived.material_property_names": ["Generic", "SAE 1020", "density measure"],
"derived.material_property_units": ["gram^1*centimetre^3"],
"derived.material_property_numeric_values": [1, 7.84],
"derived.validation_property_count": 0,
"derived.skipped_entity_ids": []
}
}
}
]
}
+259 -16
View File
@@ -68,6 +68,13 @@
* ("/tc6_reg.c/sub6.s#sub"), so the lookup missed and no variant was
* applied, leaving the phantom face. After the fix the plan is built
* from region roots so keys match and the phantom face is eliminated.
*
* TC7 - NMG Boolean isolation and output modes: verifies that an interrupted
* worker cannot commit output, then exercises BoT, NMG, region, and
* in-place output paths.
*
* TC8 - implicit solid roots survive the working-database close and remain
* valid members of the imported output hierarchy.
*/
#include "common.h"
@@ -78,7 +85,9 @@
#include <string>
#include "bu/app.h"
#include "bu/env.h"
#include "bu/file.h"
#include "bu/log.h"
#include "bu/malloc.h"
#include "bu/str.h"
#include "bu/vls.h"
@@ -142,6 +151,36 @@ write_comb2(struct rt_wdb *wdbp,
0, 0, 0, 0, 0, 0, 0);
}
static int
write_comb1(struct rt_wdb *wdbp, const char *comb_name, const char *member, int is_region)
{
struct bu_list head;
BU_LIST_INIT(&head);
mk_addmember(member, &head, NULL, WMOP_UNION);
return mk_comb(wdbp, comb_name, &head, is_region, NULL, NULL, NULL,
0, 0, 0, 0, 0, 0, 0);
}
static int
run_facetize_command(const char *gfile, int argc, const char **argv, int verbose)
{
struct ged *gedp = ged_open("db", gfile, 1);
if (!gedp) {
bu_log("[regress_facetize] ged_open(%s) failed\n", gfile);
return BRLCAD_ERROR;
}
int ret = ged_exec(gedp, argc, argv);
if (verbose || ret != BRLCAD_OK) {
const char *log = bu_vls_cstr(gedp->ged_result_str);
if (log && log[0])
bu_log("[facetize] %s\n", log);
}
ged_close(gedp);
return ret;
}
/**
* Open @a gfile with ged_open and run "facetize @a input @a output".
* Returns BRLCAD_OK on success, BRLCAD_ERROR otherwise.
@@ -149,23 +188,8 @@ write_comb2(struct rt_wdb *wdbp,
static int
run_facetize(const char *gfile, const char *input, const char *output, int verbose)
{
struct ged *gedp = ged_open("db", gfile, 1);
if (!gedp) {
bu_log("[regress_facetize] ged_open(%s) failed\n", gfile);
return BRLCAD_ERROR;
}
const char *av[4] = {"facetize", input, output, NULL};
int ret = ged_exec(gedp, 3, av);
if (verbose || ret != BRLCAD_OK) {
const char *log = bu_vls_cstr(gedp->ged_result_str);
if (log && log[0])
bu_log("[facetize] %s\n", log);
}
ged_close(gedp);
return ret;
return run_facetize_command(gfile, 3, av, verbose);
}
/**
@@ -212,6 +236,46 @@ bu_log("[regress_facetize] check_bot: '%s' type %d is not a BoT\n",
return ok;
}
static int
check_object_type(const char *gfile, const char *object_name, int expected_type)
{
struct db_i *dbip = db_open(gfile, DB_OPEN_READONLY);
if (!dbip || db_dirbuild(dbip) < 0) {
if (dbip)
db_close(dbip);
return BRLCAD_ERROR;
}
struct directory *dp = db_lookup(dbip, object_name, LOOKUP_QUIET);
if (!dp) {
bu_log("[regress_facetize] '%s' does not exist\n", object_name);
db_close(dbip);
return BRLCAD_ERROR;
}
struct rt_db_internal intern;
RT_DB_INTERNAL_INIT(&intern);
if (rt_db_get_internal(&intern, dp, dbip, NULL) < 0) {
db_close(dbip);
return BRLCAD_ERROR;
}
int ret = (intern.idb_minor_type == expected_type) ? BRLCAD_OK : BRLCAD_ERROR;
if (ret != BRLCAD_OK)
bu_log("[regress_facetize] '%s' type %d, expected %d\n",
object_name, intern.idb_minor_type, expected_type);
rt_db_free_internal(&intern);
db_close(dbip);
return ret;
}
static int
count_log_hook(void *data, void *UNUSED(message))
{
int *count = (int *)data;
(*count)++;
return 0;
}
/**
* Run rt_crofton_shoot on @a obj_name in @a gfile.
* Stores SA (mm2) and VOL (mm3) in *sa / *vol.
@@ -1120,6 +1184,183 @@ tc6_havoc_wind9_pattern(const char *tmpdir, int verbose)
return BRLCAD_OK;
}
static int
tc7_nmg_modes(const char *tmpdir, int verbose)
{
bu_log("[regress_facetize] TC7: NMG Boolean and output modes...\n");
struct bu_vls gpath = BU_VLS_INIT_ZERO;
bu_vls_printf(&gpath, "%s/tc7_nmg.g", tmpdir);
const char *gfile = bu_vls_cstr(&gpath);
if (bu_file_exists(gfile, NULL))
bu_file_delete(gfile);
struct db_i *dbip = db_create(gfile, 5);
if (!dbip) {
bu_vls_free(&gpath);
return BRLCAD_ERROR;
}
struct rt_wdb *wdbp = wdb_dbopen(dbip, RT_WDB_TYPE_DB_DEFAULT);
point_t box_pts[8] = {
{0.0, 0.0, 0.0}, {10.0, 0.0, 0.0},
{10.0, 10.0, 0.0}, {0.0, 10.0, 0.0},
{0.0, 0.0, 10.0}, {10.0, 0.0, 10.0},
{10.0, 10.0, 10.0}, {0.0, 10.0, 10.0}
};
if (write_arb8(wdbp, "box.s", box_pts) < 0 ||
write_comb1(wdbp, "box.r", "box.s", 1) < 0 ||
write_comb1(wdbp, "box.c", "box.s", 0) < 0) {
wdb_close(wdbp);
bu_vls_free(&gpath);
return BRLCAD_ERROR;
}
wdb_close(wdbp);
const char *bot_av[] = {"facetize", "--nmg-booleval", "box.r", "box.bot", NULL};
const char *fault_av[] = {"facetize", "--nmg-booleval", "box.r", "fault.bot", NULL};
const char *nmg_av[] = {"facetize", "-n", "box.r", "box.nmg", NULL};
const char *regions_av[] = {"facetize", "-r", "--nmg-booleval", "box.r", "box.regions", NULL};
const char *fallback_av[] = {"facetize", "-r", "--nmg-booleval", "box.c", "box-fallback.bot", NULL};
const char *object_in_place_av[] = {"facetize", "--in-place", "--nmg-booleval", "box.c", NULL};
const char *in_place_av[] = {"facetize", "-r", "--in-place", "--nmg-booleval", "box.r", NULL};
int ret = BRLCAD_OK;
auto check_tc7 = [&ret](bool passed, const char *description) {
if (passed)
return;
bu_log("[regress_facetize] TC7: FAIL - %s\n", description);
ret = BRLCAD_ERROR;
};
struct bu_vls fault_path = BU_VLS_INIT_ZERO;
bu_vls_printf(&fault_path, "%s/tc7_nmg_worker.fault", tmpdir);
(void)bu_file_delete(bu_vls_cstr(&fault_path));
(void)bu_setenv("LIBGED_FACETIZE_TEST_FAULT",
"nmg-before-ready:fault.bot", 1);
(void)bu_setenv("LIBGED_FACETIZE_TEST_FAULT_FILE",
bu_vls_cstr(&fault_path), 1);
int fault_ret = run_facetize_command(gfile, 4, fault_av, verbose);
(void)bu_setenv("LIBGED_FACETIZE_TEST_FAULT", "", 1);
(void)bu_setenv("LIBGED_FACETIZE_TEST_FAULT_FILE", "", 1);
check_tc7(fault_ret == BRLCAD_ERROR,
"an interrupted NMG Boolean worker did not report failure");
check_tc7(bu_file_exists(bu_vls_cstr(&fault_path), NULL),
"NMG Boolean worker fault did not trigger");
check_tc7(check_object_type(gfile, "fault.bot", ID_BOT) != BRLCAD_OK,
"an interrupted NMG Boolean worker committed an output");
(void)bu_file_delete(bu_vls_cstr(&fault_path));
bu_vls_free(&fault_path);
int hook_calls = 0;
bu_log_add_hook(count_log_hook, &hook_calls);
int bot_ret = run_facetize_command(gfile, 4, bot_av, verbose);
int hook_calls_before_probe = hook_calls;
bu_log("facetize NMG hook restoration probe\n");
bu_log_delete_hook(count_log_hook, &hook_calls);
check_tc7(bot_ret == BRLCAD_OK, "NMG Boolean BoT command failed");
check_tc7(hook_calls == hook_calls_before_probe + 1,
"temporary log hook was not restored");
check_tc7(check_bot_exists(gfile, "box.bot") == BRLCAD_OK,
"NMG Boolean BoT output is missing");
int nmg_ret = run_facetize_command(gfile, 4, nmg_av, verbose);
check_tc7(nmg_ret == BRLCAD_OK, "NMG output command failed");
check_tc7(check_object_type(gfile, "box.nmg", ID_NMG) == BRLCAD_OK,
"NMG output has the wrong type");
int regions_ret = run_facetize_command(gfile, 5, regions_av, verbose);
check_tc7(regions_ret == BRLCAD_OK, "NMG Boolean region command failed");
check_tc7(check_object_type(gfile, "box.regions", ID_COMBINATION) == BRLCAD_OK,
"NMG Boolean region output is missing");
int fallback_ret = run_facetize_command(gfile, 5, fallback_av, verbose);
check_tc7(fallback_ret == BRLCAD_OK,
"region mode object fallback command failed");
check_tc7(check_bot_exists(gfile, "box-fallback.bot") == BRLCAD_OK,
"region mode object fallback output is missing");
int object_in_place_ret = run_facetize_command(gfile, 4,
object_in_place_av, verbose);
check_tc7(object_in_place_ret == BRLCAD_OK,
"in-place NMG Boolean object command failed");
check_tc7(check_object_type(gfile, "box.c", ID_BOT) == BRLCAD_OK,
"in-place NMG Boolean object has the wrong type");
int in_place_ret = run_facetize_command(gfile, 5, in_place_av, verbose);
check_tc7(in_place_ret == BRLCAD_OK,
"in-place NMG Boolean region command failed");
check_tc7(check_object_type(gfile, "box.r", ID_COMBINATION) == BRLCAD_OK,
"in-place region root has the wrong type");
check_tc7(check_bot_exists(gfile, "box.r.bot") == BRLCAD_OK,
"in-place region BoT output is missing");
bu_log("[regress_facetize] TC7: %s\n", ret == BRLCAD_OK ? "PASS" : "FAIL");
bu_vls_free(&gpath);
return ret;
}
/* ------------------------------------------------------------------ */
/* TC8: implicit root replacement persists across database reopen */
/* ------------------------------------------------------------------ */
static int
tc8_implicit_root_persistence(const char *tmpdir, int verbose)
{
bu_log("[regress_facetize] TC8: implicit root persistence...\n");
struct bu_vls gpath = BU_VLS_INIT_ZERO;
bu_vls_printf(&gpath, "%s/tc8_implicit_root.g", tmpdir);
const char *gfile = bu_vls_cstr(&gpath);
if (bu_file_exists(gfile, NULL))
bu_file_delete(gfile);
struct db_i *dbip = db_create(gfile, 5);
if (!dbip) {
bu_vls_free(&gpath);
return BRLCAD_ERROR;
}
struct rt_wdb *wdbp = wdb_dbopen(dbip, RT_WDB_TYPE_DB_DEFAULT);
point_t region_points[8] = {
{0.0, 0.0, 0.0}, {10.0, 0.0, 0.0},
{10.0, 10.0, 0.0}, {0.0, 10.0, 0.0},
{0.0, 0.0, 10.0}, {10.0, 0.0, 10.0},
{10.0, 10.0, 10.0}, {0.0, 10.0, 10.0}
};
point_t loose_points[8] = {
{20.0, 0.0, 0.0}, {30.0, 0.0, 0.0},
{30.0, 10.0, 0.0}, {20.0, 10.0, 0.0},
{20.0, 0.0, 10.0}, {30.0, 0.0, 10.0},
{30.0, 10.0, 10.0}, {20.0, 10.0, 10.0}
};
if (!wdbp || write_arb8(wdbp, "region.s", region_points) < 0 ||
write_arb8(wdbp, "loose.s", loose_points) < 0 ||
write_comb1(wdbp, "box.r", "region.s", 1) < 0 ||
write_comb2(wdbp, "assembly.c", "box.r", 'u',
"loose.s", 'u') < 0) {
if (wdbp)
wdb_close(wdbp);
bu_vls_free(&gpath);
return BRLCAD_ERROR;
}
wdb_close(wdbp);
const char *facetize_args[] = {
"facetize", "-r", "assembly.c", "assembly.regions", NULL
};
int ret = run_facetize_command(gfile, 4, facetize_args, verbose);
if (ret == BRLCAD_OK &&
(check_object_type(gfile, "assembly.regions", ID_COMBINATION) != BRLCAD_OK ||
check_object_type(gfile, "facetize_assembly.c", ID_COMBINATION) != BRLCAD_OK ||
check_bot_exists(gfile, "box.r.bot") != BRLCAD_OK ||
check_bot_exists(gfile, "facetize_loose.s") != BRLCAD_OK))
ret = BRLCAD_ERROR;
bu_log("[regress_facetize] TC8: %s\n",
ret == BRLCAD_OK ? "PASS" : "FAIL");
bu_vls_free(&gpath);
return ret;
}
/* ------------------------------------------------------------------ */
/* main */
/* ------------------------------------------------------------------ */
@@ -1151,6 +1392,8 @@ return 1;
if (tc4_havoc_reuse(tmpdir, verbose) != BRLCAD_OK) ret = 1;
if (tc5_near_coplanar_subTOL(tmpdir, verbose) != BRLCAD_OK) ret = 1;
if (tc6_havoc_wind9_pattern(tmpdir, verbose) != BRLCAD_OK) ret = 1;
if (tc7_nmg_modes(tmpdir, verbose) != BRLCAD_OK) ret = 1;
if (tc8_implicit_root_persistence(tmpdir, verbose) != BRLCAD_OK) ret = 1;
if (ret == 0)
bu_log("[regress_facetize] All tests PASSED\n");
-508
View File
@@ -1,508 +0,0 @@
Remaining STEP Import/Export Work
=================================
Status: 2026-08-05
Purpose
-------
This is the current implementation and qualification plan for the public
step-g and g-step converters. It replaces recommended_practices_todo.txt,
save_invalid_plan.txt, and step_refactor.txt. It is not a development
changelog: completed work is summarized only to define the supported baseline,
and the detailed sections describe work which remains.
Status terms used below are:
* COMPLETE -- implemented and covered by focused tests;
* ACTIVE -- being qualified or reduced from real inputs;
* REMAINING -- useful work with a defined scope;
* BLOCKED -- requires a missing prerequisite and must not be approximated; and
* DEFERRED -- outside the present converter objective.
A feature is complete only when schema-valid fixtures, permissive and strict
policy tests, and structured report assertions prove that geometry or semantics
did not disappear. Independent-producer evidence is required wherever a
BRL-CAD export and re-import could otherwise repeat the same mistake.
Supported Baseline
------------------
Architecture and schemas
~~~~~~~~~~~~~~~~~~~~~~~~
The public converter architecture is COMPLETE:
* step-g and g-step are schema-dispatching hosts;
* AP203 edition 1, AP203 edition 2, AP214 edition 3, and AP242 editions 1
through 4 are bidirectional plugins;
* AP242 generated bindings remain edition-specific because the entity sets and
some Part 21 physical layouts differ;
* the AP242 export alias targets edition 4, while automatic import probes
editions 4 through 1;
* common conversion behavior is schema-neutral and schema-only behavior is
under ap203, ap203e2, ap214, or ap242; and
* obsolete schema-specific public converter programs are not built.
Explicit --schema ap242e1 through ap242e4 selection remains useful when a file
uses only the editions' common physical subset. Interim identifiers
CONFIGURATION_CONTROL_3D_DESIGN_MIM_LF, CCD_CLA_GVP_AST, and
CCD_CLA_GVP_AST_ASD route to AP203e2. Supported multi-schema AP203e2 headers
also route correctly; unknown companion schemas remain visible, and genuinely
ambiguous supported-AP combinations fail rather than guessing.
The STEPcode lazy reader supports multiple DATA sections, SCOPE,
source-ordered complex components, bounded diagnostics, type census output,
and schema-independent retention of unknown configuration records. AP203
Recommended Practice 8 fixtures cover referenced, mapped, and replicated
shapes, multi-level hierarchy, SHUO and usage semantics, multiple SDR forms,
wireframes, configuration data, and SCOPE.
STEPcode and build tractability
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
BRL-CAD uses STEPcode's compact generated API v2, packed schema modules,
precomputed attribute layouts, bounded unity chunks, and generated source
manifests. Entity and type chunk limits are independently tunable; 64:8 is the
qualified default. STEP plugins retain optimized compilation but disable LTO.
On the qualification host, a serial AP242e4 binding build without LTO fell
from 8m25s and 6.22 GB peak RSS at 64:64 to 6m12s and 1.33 GB at 64:8. NIST
CTC 03 showed no measurable conversion benefit from LTO.
The importer is split into schema-neutral common build units for session and
cache handling, document traversal, job scheduling, geometric sets, topology,
periodic bands, pullback, seam repair, loop repair, validation, and
finalization. Schema plugins consume those objects directly, preserving
static Factory registration without a new runtime ABI. A local geometry
repair edit therefore recompiles its topical unit and relinks only requested
plugins. Revisit these boundaries only in response to a measured dependency,
ownership, or rebuild problem.
Default policy and loss accounting
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The user-facing default is deliberately permissive:
* safe topology repair is enabled;
* exact and strict modes are disabled;
* storage-safe invalid BReps are preserved and explicitly tagged;
* useful partial output is published with status 1 and a structured report;
* --strict is transactional; and
* --reject-invalid-objs restores invalid-BRep rejection.
Recovery order is source-faithful construction, bounded safe repair, tagged
modeling inference for a proven source contradiction, marked storage-safe
invalid fallback, and finally omission. Scale-bounded inference is attempted
only in a detached whole-item retry after the ordinary candidate fails. It is
committed only when either:
* the rebuilt BRep passes normal structural and solidness validation; or
* permissive invalid preservation proves that a structurally safe open result
corresponds one-for-one to source EDGE_CURVEs with exactly one authored
ORIENTED_EDGE use. Such a result remains invalid and is tagged
`step:invalidity=authored_open_topology`; it is never presented as a solid.
Inference is disabled by exact, strict, no-repair, and reject-invalid policy.
Reports and affected database objects retain source edge identifiers,
discrepancy, safe and inference limits, declared tolerance, inference kind,
and explanatory detail. Rejected inference transactions do not contaminate
ordinary results or diagnostics.
Every product-bound representation, representation item, selected BRL-CAD
object, and reachable child receives a handled, intentionally non-geometric,
skipped, malformed, unsupported, or failed disposition. Import and export
distinguish complete, partial, empty, and failed outcomes. The pre-conversion
entity census describes the input and does not imply conversion support.
A preserved invalid BRep is not, by itself, proof of bad authoring. Unless an
independent topology proof establishes a source contradiction, it remains an
unresolved converter result tagged `step:geometry_status=invalid_preserved`
and `step:source_validity=unresolved`. It forces a partial result and remains
in the failure ratchet.
Geometry
~~~~~~~~
The following geometry paths are COMPLETE for their documented subsets:
* rigid product hierarchies, repeated occurrences, transforms, and shared
product/occurrence emission in every supported schema;
* optional native CSG import/export in AP203e2, AP214, and AP242, including
supported Boolean trees, exact mapped primitives, BRep leaves, transforms,
and RECTANGULAR_PYRAMID;
* exact compatible extruded and revolved area/face solids and linear or
circular SWEPT_DISK_SOLID in AP203e2, AP214, and AP242;
* AP242 triangulated faces, simple/complex surface sets, shells, and solids,
including strips, fans, pnindex, normals, welding, closure, and winding;
* analytic and spline BRep import, periodic and singular topology, voids,
bounded repair, transactional inference, and explicit invalid fallback;
* solid-BRep export/re-import for sphere, cylinder, cone, torus, box, open
NURBS faces with voids, rational Cobb surfaces, and BREP_WITH_VOIDS;
* mated librt BRep fallback for EHY, EPA, HYP, PART, PIPE, EXTRUDE, REVOLVE,
solid BOT, closed ARS, and supported NMG input;
* sketch recovery for reordered or reversed segments, zero-length segments,
inner loops, lines, circles, arcs, Beziers, and rational/non-rational NURBS;
and
* mixed GEOMETRIC_SET curves, points, and CURVE_BOUNDED_SURFACEs with partial
set loss accounting and shared bounded pullback validation.
The detailed native-CSG primitive and operator matrix is maintained in
src/conv/step/README.csg.md. AP203e1 has no native-CSG path; non-union AP203e1
trees are BLOCKED on reliable Boolean evaluation.
STEP B-spline curves and surfaces share exact conversion from EXPRESS full knot
vectors to OpenNURBS endpoint-truncated storage. Valid unclamped closed
surfaces are clamped and only numerically equal seam controls are normalized,
subject to dense before/after audits.
Presentation, units, configuration, and PMI
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
AP203e2, AP214, and AP242 share colour, transparency, style, layer, material,
property, validation-value, and recursive unit-graph extraction. Export
round-trips product identity and revision, presentation, multiple materials,
typed properties, known SI and exact conversion units, derived units,
occurrence labels, and reference designators. AP203e1 explicitly reports
presentation or property requests it cannot author.
One constrained safe-mode repair handles an identified producer defect where
a positive density's complete unit graph is mass^1 times length^3 instead of
mass^1 times length^-3. The value is retained, the denominator dimension is
corrected, provenance is recorded, and exact, strict, and no-repair modes
reject the graph. This is not a general unit-guessing rule.
--output-units, --angle-units, and --uncertainty form one schema-neutral export
policy covering BReps, native CSG, cone angles, and rigid occurrence
translations.
Configuration data is retained as a typed reference graph with exact Part 21
right-hand sides. Cross-schema authoring covers approvals, provenance,
assignments, effectivity, alternatives, substitutes, categories, people,
organizations, addresses, security classifications, documents, date/time
chains, and their legal schema-specific assignment forms.
DATUM supports point, line, plane, frame, and bounded-target forms, semantic
roles, axes, dimensions, identifiers, validation, transforms, and versioned
legacy-compatible database storage. ANNOT supports model-space planes,
per-segment roles and styles, UTF-8 text, runtime TrueType outlines,
transforms, and legacy screen-space compatibility. The hardened private
struetype code is shared from libbu; LGPL OSIFont remains runtime data and is
not compiled into a binary.
AP242 PMI stage 1 retains connected semantic, presentation, association, and
dependency records with source values, component order, references, product
ownership, and native disposition. Exactly resolved point, axis, and plane
datums become DATUM objects. Supported graphical content becomes model-space
ANNOT geometry; ambiguous content remains semantic-only. Malformed
presentation is partial in permissive mode and transactional in strict mode.
Safety and test infrastructure
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
BRL-CAD and STEPcode support ASan+UBSan and a separate TSan mode. Release and
ASan+UBSan have covered the complete STEP/AP range; focused parallel, failure,
native-CSG, and BRep-export paths have run under TSan. On the qualification
host TSan must run through `setarch x86_64 -R` because its runtime otherwise
fails before main. Repeated -j8 versus -j1 output comparison remains part of
the determinism gate.
Validation Snapshot
-------------------
In-tree tests include success, malformed, partial, strict, round-trip,
sanitizer, and output-validity cases across every supported schema. The last
complete focused Release STEP matrix contains 197 tests. Official NIST MBE PMI
files are stored under db/nist with documented provenance. Five AP203 MIM
files exercise AP203e2, and 17 AP242 files span editions 1 through 4.
Checksum-pinned, non-redistributed STEP Tools and STEPcode samples cover CATIA,
Unigraphics, AutoCAD, I-DEAS, STEPnet, and STEP Tools producers. Important
independent modern-schema evidence currently includes:
* AP242 Leopard2-A6: 31/31 geometry items, 14 products, 338 occurrences,
2,425 styles, 13 material identities, and 26 material values;
* AP242 Open Source Rover: six product levels, 713 products, 1,390 rigid
occurrences, and 569 applied styles; and
* AP203e2 ROTATOR_ASM: eight product levels, 126 participating products, 404
usages, 671 product-property records, and 3,991 styles.
The last complete unified corpus baseline is the 1,138-file dry run at
`.build/step-corpus-full-j8-validity-ladder-20260804-post-speaker`:
* schemas: 616 AP203, 375 AP214, four AP203e2, and 143 AP242 files;
* files: 1,057 clean; process statuses were 1,064 complete, 69 useful partial,
three intentionally empty, one failed, and one SIGSEGV;
* geometry: 41,765 of 41,992 attempted items written (99.46 percent), 221 exact
omissions, and six unsupported-representation omissions;
* invalid fallback: 386 structurally invalid but storage-safe BReps preserved,
none rejected;
* aggregate wall/conversion time: 36,796.2/34,678.9 seconds; and
* maximum per-file RSS: 3.62 GB.
This remains the comparison baseline, not the current expected result. The
crash and several omission groups have since been fixed in selected working
runs. Its timing delta from the preceding baseline is not a controlled
regression result because host load and available memory differed materially.
Preserve both manifests and compare future runs only under similar load.
The corpus runner records hashes, schemas, converter/plugin/shared-library
identities, reports, logs, wall time, peak RSS, host load, memory availability,
geometry outcomes, hierarchy depth, metadata, and failure categories. Dry
mode still parses, constructs, validates, and repairs geometry; it omits only
database serialization. Historical `.g` files are comparative artifacts, not
correctness oracles, and must remain intact.
Selected post-baseline qualifications establish:
* cargo ship now terminates normally; its two remaining selected omissions are
explicit missing required FACE_SURFACE references;
* hAVYARD now terminates normally after rejected seam transactions reacquire
topology owned by the restored BRep; a direct owned lifetime fixture remains
to be added;
* SNAP #801290 is a valid solid after a face-scoped horn-torus keyhole repair;
* all ten historical T90 omissions serialize: six valid solids, two
source-open BReps tagged `authored_open_topology`, and two storage-safe
partial fallbacks whose contradictions exceed bounded inference limits;
* paired 135-file AP203/AP242 Recurta runs write all 139 attempted items per
schema as valid BReps, with three tagged inferences per schema; and
* the selected 421-face speaker solid now produces a validated 419-face solid
through a whole-item shared-spur transaction.
The T90 serialized selection is recorded in
`.build/t90_omissions_final_write.json`; it attempted and wrote ten of ten,
skipped none, and explicitly preserved four invalid objects. The file-level
partial status is intentional and does not represent an omission.
Two measured performance targets remain useful:
* Recurta `10012.step`, 992 faces: 78.7 seconds in the final qualification;
profiling attributes most time to periodic trim-orientation repair and
repeated transaction work, not preflight construction; and
* chess king, 13,234 faces: about 616.8 seconds and 2.92 GB in its selected
clean run.
Remaining Work in Recommended Order
-----------------------------------
1. Close the current corpus ratchet -- ACTIVE
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Complete selected work before starting another expensive unified run:
1. Add a compact rejected-seam-transaction lifetime fixture, qualify it under
ASan+UBSan, and retain the complete hAVYARD real-file check.
2. Sample and reduce the largest post-baseline omission sets:
* Garrett: 49;
* 2-litre engine: 49, including 48 GEOMETRIC_SET items;
* Open Source Rover: 16;
* Epoxy granite VMC: seven; and
* BLANCO: five.
3. Retest the former 58-omission GEOMETRIC_SET group with the completed bounded
surface path. The independent b/c/d compatibility files are already
qualified except for six d-wireframe boundary/pcurve disagreements.
4. Reduce the Leopard2-A6 export/re-import failure at STEP edge #460691. One
of 31 exported BReps fails while splitting a pcurve whose candidate
fragment is 36.4476 model units from its subedge. This is a geometry
interoperability problem, not schema selection or assembly loss.
5. Retain MOUSE as a source edge/surface mismatch unless independent geometry
proves a narrower correction.
6. Revisit T90 #20240 and #20307 only if independent geometry supports a
narrower reconstruction. Their marked partial results are preferable to
inventing a false solid.
7. Remove repeated full-BRep scans and copies from large periodic repair
transactions while preserving exact rollback behavior.
8. Run focused Release, ASan+UBSan, TSan where applicable, and -j1/-j8
determinism gates. Then run the complete 1,138-file qualification.
Corpus fixes require a compact reproducer, permissive and strict assertions,
structured report coverage, and a selected real-file check. Do not introduce
broad repair heuristics solely to improve corpus percentages.
The next unified run is acceptable only if every file reaches a normal terminal
report; there is no crash, hang, schema-load failure, corrupt output, or
unexplained zero-geometry result; and all omissions and invalid fallbacks are
grouped by owned root cause.
2. Expand independent AP203e2/AP242 evidence -- REMAINING
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Locate and pin independent examples for:
* AP203e2 material and property association variants;
* AP242 materials beyond the Inventor MAKE_FROM_USAGE_OPTION pattern;
* mixed representation contexts and large multiple-product files; and
* AP203e2/AP242 sweeps, NURBS, shells, voids, and mixed BRep/CSG assemblies.
Promote small distributable reductions into in-tree fixtures. For external
inputs record source, producer, SHA-256, and structured expectations. A
BRL-CAD-authored round trip alone cannot close this item.
3. Complete AP242 semantic PMI and MBD -- REMAINING
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Stage 1 graph retention and exact native point/axis/plane/curve creation are
complete. Remaining interpretation and authoring work is to:
* normalize dimension type/value, tolerance type and magnitude,
material-condition modifiers, datum-reference compartments, and zones;
* preserve stable product, occurrence, face, edge, and vertex target links;
* distinguish semantic-only, graphical-only, and validated combined PMI;
* map representable text, leaders, extension/dimension lines, arrowheads,
fills, area targets, and supported symbols;
* explicitly report graphical content which cannot be mapped faithfully;
* author retained semantic and supported presentation records in AP242; and
* qualify against NIST PMI and independent AP242 files, not only hand-authored
fixtures.
An ANNOT outline is not proof that original text or tolerance semantics were
recovered. Do not infer missing semantics from graphical similarity.
4. Complete tessellated coverage and optional export -- REMAINING
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Add faithful mappings for tessellated point, curve, wire, geometric-set, and
repositioned-item forms. Add explicit optional AP242 BOT export using the
shared unit and uncertainty policy. Tessellation must not silently replace
the eventual evaluated-BRep default for analytic solids.
5. Broaden existing-BRep interoperability -- REMAINING
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Reduce the six d-wireframe boundary/pcurve disagreements and independently
classify each as an authored tolerance violation or adapter defect. Preserve
faithful partial-set output until that proof exists; do not stretch pcurves or
widen model uncertainty merely to reach complete counts.
Expand independent-producer coverage for rational and non-rational NURBS,
open shells, nested BREP_WITH_VOIDS, reversed proxies/faces, difficult trims,
periodic and singular surfaces, and closed curves. Define a legal policy for
one BRL-CAD object containing disconnected material volumes. Add wireframe
and surface-curve export for databases without face topology.
Verification must include STEPcode readback, strict BRL-CAD re-import,
topology and material-ray comparison, and an independent STEP validator or
consumer where available. Textual identity is insufficient.
Sketch containment still needs a complete loop hierarchy for disjoint outer
regions and nested material islands. REVOLVE should split profiles at sharp
C0/tangent discontinuities when needed to avoid pathological wireframe and CDT
refinement.
DSP, deprecated HF, EBM, and VOL are a separate scalability and fidelity
project. They require bounded-error patching or faithful faceting, discrete
boundary simplification, source-semantic ray/material comparisons, and hard
validity, memory, and complexity limits. A single high-degree DSP surface or
unbounded voxel-to-BRep expansion is not an acceptable general solution.
6. Complete product and configuration mappings -- REMAINING
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Define scoped mappings and loss policy for region identifiers,
material/shader strings, and selected arbitrary BRL-CAD attributes. Do not
conflate opaque attributes with typed STEP properties.
Broaden normalized named-unit conveniences only where schemas agree; the
recursive retained graph remains authoritative. Extend authoring for
remaining configuration identities, relationships, complex addresses, and
application edges only when required source values exist. Expand
import-export-import tests to multiple products, occurrences, shared complex
effectivities, and schema-specific graphs.
7. Resolve assembly and native-CSG edge cases -- REMAINING
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Define explicit policy for scale, mirror, and future non-rigid occurrence
transforms. Preserve or explicitly report nested combination identity inside
one CSG expression; STEP CSG_SELECT cannot directly use another CSG_SOLID as
an operand. Expand stable naming and independent multi-level mixed BRep/CSG
evidence across AP203e2, AP214, and AP242.
NOT and guard/no-op Boolean nodes have no lossless finite STEP CSG encoding.
AP242 CYCLIDE_SEGMENT_SOLID remains unsupported because recognizing a faithful
BRL-CAD PIPE subset requires additional geometric semantics; similar
dimensions alone are insufficient.
8. Implement remaining exact sweeps -- REMAINING
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Add nonlinear swept-disk directrices only when OpenNURBS can construct a
faithful pipe with validated frame continuity and self-intersection handling.
Ellipse, spline, and composite-curve cases remain. Implement drafted and
trimmed face sweeps only after referenced termination, offset, trim intent,
and per-edge draft can be evaluated exactly. Inherited depth or angle is not
a faithful fallback.
Blocked and Deferred Work
-------------------------
Evaluated-BRep default export -- BLOCKED
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The broadly interoperable default should eventually evaluate arbitrary
Boolean trees into final BReps. Current Boolean evaluation is not reliable
enough. Continue evaluator work and validation; when it is ready, make
evaluated BRep the default without weakening optional exact native CSG.
STEPcode upstreaming -- DEFERRED maintenance
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The generated-binding scalability work required by step-g and g-step is
complete. Remaining STEPcode work is upstream preparation and maintenance:
retain API v1 compatibility where required, and address correctness, cache,
generation, or resource defects when measured. Reopen generator scalability
only for a demonstrated regression or a new schema exceeding current limits.
Additional application protocols -- DEFERRED
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Do not build every ISO SMRL schema speculatively. AP224 machining features and
AP238 STEP-NC are the strongest future geometry/manufacturing candidates once
representative inputs and mapping goals exist. AP209 analysis and AP210
electronics require domain-specific semantics. AP232 and AP239 are primarily
metadata protocols and are not geometry targets merely because they share
integrated resources.
ISO SMRL and historical STEPmod selection, provenance, and redistribution
policy are documented in STEP(5).
Definition of Done
------------------
Every new decoder, emitter, or repair must include, as applicable:
1. an independent hand-authored or external fixture;
2. a supported case and a malformed or explicitly unsupported case;
3. permissive and transactional strict assertions;
4. structured report checks proving exhaustive loss accounting;
5. schema-specific coverage where legal alternatives or physical layouts
differ;
6. round-trip geometry, topology, semantic, and unit checks;
7. Release, sanitizer, and deterministic parallel tests;
8. selected corpus reruns for every affected real file; and
9. matching step-g, g-step, STEP(5), CSG matrix, and plan documentation.
The overall STEP effort is broadly robust when:
* every input and selected output object is exhaustively accounted for;
* the unified corpus has no crash, hang, schema-load failure, corrupt output,
or unexplained zero-geometry result;
* AP203e2 and AP242 have independent producer evidence comparable to the
relevant AP203/AP214 baseline;
* supported tessellation, rigid assemblies, presentation, configuration, and
PMI round-trip with explicit losses;
* existing BReps interoperate across analytic, NURBS, periodic, singular,
shell, void, orientation, and trim cases with independent consumers;
* native CSG remains optional, exact, and explicit about unsupported content;
* parallel, sanitizer, deterministic-output, performance, and memory gates
remain green; and
* reliable Boolean evaluation enables evaluated-BRep default export without
regressing native CSG.
+2 -1
View File
@@ -29,6 +29,7 @@ set(
api.c
centroid.c
check_options.c
contour.cpp
density.cpp
globals.c
inside.c
@@ -68,7 +69,7 @@ set(
cmakefiles(${libanalyze_ignored_files})
# Note - libanalyze_deps is defined by ${BRLCAD_SOURCE_DIR}/src/source_dirs.cmake
brlcad_addlib(libanalyze "${LIBANALYZE_SOURCES}" "" "${CMAKE_CURRENT_BINARY_DIR}"
brlcad_addlib(libanalyze "${LIBANALYZE_SOURCES}" "" "${CMAKE_CURRENT_BINARY_DIR};${EIGEN3_INCLUDE_DIR}"
PUBLIC_LIBS ${libanalyze_deps}
)
set_target_properties(libanalyze PROPERTIES VERSION 20.0.1 SOVERSION 20)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+4
View File
@@ -1,3 +1,6 @@
brlcad_addexec(analyze_contour contour.cpp "libanalyze;libwdb;librt;libbg;libbu" TEST)
brlcad_add_test(NAME analyze_contour COMMAND analyze_contour)
brlcad_addexec(analyze_raydiff raydiff.c "libanalyze;librt;libbu" TEST)
brlcad_addexec(analyze_sp solid_partitions.c "libanalyze;librt;libbu" TEST)
brlcad_addexec(analyze_nhit nhit.cpp "libanalyze;librt;libbu" TEST_USESDATA)
@@ -13,6 +16,7 @@ brlcad_add_test(NAME analyze_densities_std COMMAND analyze_densities std)
cmakefiles(
CMakeLists.txt
arbs.g
contour.cpp
raydiff.g
)
+424
View File
@@ -0,0 +1,424 @@
/* A N A L Y Z E _ C O N T O U R . C P P
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED.
*/
/** @file contour.cpp
*
* Basic topology and resource-limit tests for analyze_mdc().
*/
#include "common.h"
#include <climits>
#include <cmath>
#include <cstdio>
#include <set>
#include <utility>
#include "bu/app.h"
#include "bu/malloc.h"
#include "bg/trimesh.h"
#include "analyze/contour.h"
#include "analyze/polygonize.h"
#include "raytrace.h"
#include "wdb.h"
static constexpr double METRIC_RELATIVE_TOLERANCE = 0.10;
static int failures = 0;
static void
expect(bool condition, const char *message)
{
if (condition)
return;
std::fprintf(stderr, "FAIL: %s\n", message);
failures++;
}
static void
expect_metric(double actual, double expected, const char *object,
const char *metric)
{
double relative_error = std::fabs(actual - expected) / expected;
if (actual >= 0.0 && relative_error <= METRIC_RELATIVE_TOLERANCE)
return;
std::fprintf(stderr,
"FAIL: %s %s is %.17g, expected %.17g (relative error %.2f%%)\n",
object, metric, actual, expected, 100.0 * relative_error);
failures++;
}
static void
check_mesh(struct db_i *dbip, const char *object, int expected_components = 1,
double expected_area = 0.0, double expected_volume = 0.0,
size_t minimum_faces = 4, int expected_euler = INT_MAX)
{
int *faces = NULL;
point_t *vertices = NULL;
size_t face_count = 0;
size_t vertex_count = 0;
struct analyze_mdc_params params = ANALYZE_MDC_PARAMS_DEFAULT;
params.min_depth = 4;
params.max_depth = 8;
params.max_rays = 0;
params.minimum_free_mem = 0;
params.max_time = 0;
params.verbosity = 0;
enum analyze_mdc_status status = analyze_mdc(&faces, &face_count,
&vertices, &vertex_count, object, dbip, &params);
expect(status == ANALYZE_MDC_OK, object);
if (status == ANALYZE_MDC_OK) {
expect(vertex_count >= 4, "contour has vertices");
expect(face_count >= minimum_faces, "contour has expected resolution");
expect(bg_trimesh_solid2(static_cast<int>(vertex_count),
static_cast<int>(face_count),
reinterpret_cast<fastf_t *>(vertices), faces, NULL) == 0,
"contour is a closed oriented manifold");
if (expected_euler != INT_MAX) {
std::set<std::pair<int, int>> edges;
for (size_t face = 0; face < face_count; face++) {
for (size_t corner = 0; corner < 3; corner++) {
int a = faces[3 * face + corner];
int b = faces[3 * face + (corner + 1) % 3];
edges.insert(std::minmax(a, b));
}
}
long long euler = static_cast<long long>(vertex_count) -
static_cast<long long>(edges.size()) +
static_cast<long long>(face_count);
expect(euler == expected_euler,
"contour has the expected Euler characteristic");
}
if (expected_area > 0.0)
expect_metric(bg_trimesh_area(faces, face_count, vertices,
vertex_count), expected_area, object, "surface area");
if (expected_volume > 0.0)
expect_metric(bg_trimesh_volume(faces, face_count, vertices,
vertex_count), expected_volume, object, "volume");
int *face_indices = NULL;
int *component_offsets = NULL;
int components = bg_trimesh_separate(&face_indices,
&component_offsets, faces, static_cast<int>(face_count));
expect(components == expected_components,
"contour has the expected connected components");
if (face_indices)
bu_free(face_indices, "MDC test component faces");
if (component_offsets)
bu_free(component_offsets, "MDC test component offsets");
}
if (faces)
bu_free(faces, "MDC test faces");
if (vertices)
bu_free(vertices, "MDC test vertices");
}
static bool
make_region(struct rt_wdb *wdbp, const char *region, const char *solid)
{
struct wmember member;
BU_LIST_INIT(&member.l);
if (!mk_addmember(solid, &member.l, NULL, WMOP_UNION))
return false;
return mk_lcomb(wdbp, region, &member, 1, NULL, NULL, NULL, 0) == 0;
}
int
main(int UNUSED(argc), const char **argv)
{
bu_setprogname(argv[0]);
struct db_i *dbip = db_create_inmem();
expect(dbip != NULL, "create in-memory database");
if (!dbip)
return 1;
struct rt_wdb *wdbp = wdb_dbopen(dbip, RT_WDB_TYPE_DB_INMEM);
expect(wdbp != NULL, "open in-memory database");
if (!wdbp) {
db_close(dbip);
return 1;
}
constexpr double sphere_radius = 10.0;
constexpr double torus_major_radius = 10.0;
constexpr double torus_minor_radius = 2.5;
constexpr double slender_torus_major_radius = 100.0;
constexpr double slender_torus_minor_radius = 2.0;
constexpr double lower_radius = 1.5;
constexpr double upper_radius = 1.0;
point_t center = VINIT_ZERO;
point_t minimum = {-8.0, -6.0, -4.0};
point_t maximum = {8.0, 6.0, 4.0};
expect(mk_sph(wdbp, "sphere.s", center, sphere_radius) == 0,
"create test sphere");
expect(mk_rpp(wdbp, "box.s", minimum, maximum) == 0,
"create test box");
point_t thin_minimum = {-5.0, -2.5, -0.05};
point_t thin_maximum = {5.0, 2.5, 0.05};
expect(mk_rpp(wdbp, "thin.s", thin_minimum, thin_maximum) == 0,
"create thin-wall test box");
point_t tunnel_minimum = {-10.0, -10.0, -10.0};
point_t tunnel_maximum = {10.0, 10.0, 10.0};
point_t tunnel_base = {0.0, 0.0, -11.0};
vect_t tunnel_height = {0.0, 0.0, 22.0};
expect(mk_rpp(wdbp, "tunnel_outer.s", tunnel_minimum,
tunnel_maximum) == 0, "create tunnel test outer box");
expect(mk_rcc(wdbp, "tunnel_void.s", tunnel_base, tunnel_height,
1.0) == 0, "create tunnel test void");
struct wmember tunnel_members;
BU_LIST_INIT(&tunnel_members.l);
expect(mk_addmember("tunnel_outer.s", &tunnel_members.l, NULL,
WMOP_UNION) != NULL, "add tunnel outer box");
expect(mk_addmember("tunnel_void.s", &tunnel_members.l, NULL,
WMOP_SUBTRACT) != NULL, "subtract tunnel void");
expect(mk_lcomb(wdbp, "tunnel.r", &tunnel_members, 1, NULL, NULL,
NULL, 0) == 0, "create tunnel test region");
vect_t normal = {0.0, 0.0, 1.0};
expect(mk_tor(wdbp, "torus.s", center, normal, torus_major_radius,
torus_minor_radius) == 0,
"create test torus");
expect(mk_tor(wdbp, "slender_torus.s", center, normal,
slender_torus_major_radius, slender_torus_minor_radius) == 0,
"create high-aspect torus");
point_t tgc_base = {-4.0, -3.0, -6.0};
vect_t tgc_height = {2.0, 1.0, 12.0};
vect_t tgc_a = {5.0, 0.0, 0.0};
vect_t tgc_b = {0.0, 3.0, 0.0};
vect_t tgc_c = {2.5, 0.0, 0.0};
vect_t tgc_d = {0.0, 1.5, 0.0};
expect(mk_tgc(wdbp, "tgc.s", tgc_base, tgc_height, tgc_a, tgc_b,
tgc_c, tgc_d) == 0, "create test TGC");
point_t concave_arb[8] = {
{500.0, -500.0, -500.0},
{500.0, 500.0, -500.0},
{500.0, -169.922, -304.688},
{500.0, -500.0, 500.0},
{400.0, -500.0, -500.0},
{400.0, 500.0, -500.0},
{400.0, -169.922, -304.688},
{400.0, -500.0, 500.0}
};
expect(mk_arb8(wdbp, "concave.s",
reinterpret_cast<const fastf_t *>(concave_arb)) == 0,
"create sharp concave ARB");
point_t lower_center = {-8.0, -5.0, -3.0};
point_t upper_center = {9.0, 4.0, 6.0};
expect(mk_sph(wdbp, "lower.s", lower_center, lower_radius) == 0,
"create lower disconnected sphere");
expect(mk_sph(wdbp, "upper.s", upper_center, upper_radius) == 0,
"create upper disconnected sphere");
struct wmember members;
BU_LIST_INIT(&members.l);
expect(mk_addmember("lower.s", &members.l, NULL, WMOP_UNION) != NULL,
"add lower disconnected sphere");
expect(mk_addmember("upper.s", &members.l, NULL, WMOP_UNION) != NULL,
"add upper disconnected sphere");
expect(mk_lcomb(wdbp, "disconnected.r", &members, 1, NULL, NULL, NULL,
0) == 0, "create disconnected region");
constexpr double overlap_radius = 5.0;
constexpr double overlap_offset = 3.0;
point_t overlap_left_center = {-overlap_offset, 0.0, 0.0};
point_t overlap_right_center = {overlap_offset, 0.0, 0.0};
expect(mk_sph(wdbp, "overlap_left.s", overlap_left_center,
overlap_radius) == 0, "create left overlapping sphere");
expect(mk_sph(wdbp, "overlap_right.s", overlap_right_center,
overlap_radius) == 0, "create right overlapping sphere");
expect(make_region(wdbp, "overlap_left.r", "overlap_left.s"),
"create left overlapping region");
expect(make_region(wdbp, "overlap_right.r", "overlap_right.s"),
"create right overlapping region");
struct wmember overlap_members;
BU_LIST_INIT(&overlap_members.l);
expect(mk_addmember("overlap_left.r", &overlap_members.l, NULL,
WMOP_UNION) != NULL, "add left overlapping region");
expect(mk_addmember("overlap_right.r", &overlap_members.l, NULL,
WMOP_UNION) != NULL, "add right overlapping region");
expect(mk_lcomb(wdbp, "overlap.c", &overlap_members, 0, NULL, NULL,
NULL, 0) == 0, "create overlapping-region combination");
vect_t box_size;
VSUB2(box_size, maximum, minimum);
const double sphere_area = 4.0 * M_PI * sphere_radius * sphere_radius;
const double sphere_volume = 4.0 * M_PI * sphere_radius * sphere_radius *
sphere_radius / 3.0;
const double box_area = 2.0 * (box_size[X] * box_size[Y] +
box_size[X] * box_size[Z] + box_size[Y] * box_size[Z]);
const double box_volume = box_size[X] * box_size[Y] * box_size[Z];
vect_t thin_size;
VSUB2(thin_size, thin_maximum, thin_minimum);
const double thin_area = 2.0 * (thin_size[X] * thin_size[Y] +
thin_size[X] * thin_size[Z] + thin_size[Y] * thin_size[Z]);
const double thin_volume =
thin_size[X] * thin_size[Y] * thin_size[Z];
const double torus_area = 4.0 * M_PI * M_PI * torus_major_radius *
torus_minor_radius;
const double torus_volume = 2.0 * M_PI * M_PI * torus_major_radius *
torus_minor_radius * torus_minor_radius;
const double slender_torus_area = 4.0 * M_PI * M_PI *
slender_torus_major_radius * slender_torus_minor_radius;
const double disconnected_area = 4.0 * M_PI *
(lower_radius * lower_radius + upper_radius * upper_radius);
const double disconnected_volume = 4.0 * M_PI *
(lower_radius * lower_radius * lower_radius +
upper_radius * upper_radius * upper_radius) / 3.0;
const double overlap_distance = 2.0 * overlap_offset;
const double overlap_cap_height = overlap_radius -
0.5 * overlap_distance;
const double overlap_intersection_volume = M_PI *
(4.0 * overlap_radius + overlap_distance) *
std::pow(2.0 * overlap_radius - overlap_distance, 2.0) / 12.0;
const double overlap_area = 8.0 * M_PI * overlap_radius *
overlap_radius - 4.0 * M_PI * overlap_radius * overlap_cap_height;
const double overlap_volume = 8.0 * M_PI * overlap_radius *
overlap_radius * overlap_radius / 3.0 -
overlap_intersection_volume;
check_mesh(dbip, "sphere.s", 1, sphere_area, sphere_volume);
check_mesh(dbip, "box.s", 1, box_area, box_volume);
check_mesh(dbip, "thin.s", 1, thin_area, thin_volume, 10000);
check_mesh(dbip, "torus.s", 1, torus_area, torus_volume, 4, 0);
check_mesh(dbip, "slender_torus.s", 1, slender_torus_area,
0.0, 4, 0);
check_mesh(dbip, "tunnel.r", 1, 0.0, 0.0, 4, 0);
check_mesh(dbip, "tgc.s");
check_mesh(dbip, "concave.s");
check_mesh(dbip, "disconnected.r", 2, disconnected_area,
disconnected_volume);
check_mesh(dbip, "overlap.c", 1, overlap_area, overlap_volume);
{
int *coarse_faces = NULL;
point_t *coarse_vertices = NULL;
size_t coarse_face_count = 0;
size_t coarse_vertex_count = 0;
struct analyze_mdc_params coarse = ANALYZE_MDC_PARAMS_DEFAULT;
coarse.min_depth = 3;
coarse.max_depth = 3;
coarse.max_rays = 50000;
coarse.minimum_free_mem = 0;
coarse.max_time = 0;
enum analyze_mdc_status coarse_status = analyze_mdc(&coarse_faces,
&coarse_face_count, &coarse_vertices, &coarse_vertex_count,
"tunnel.r", dbip, &coarse);
expect(coarse_status == ANALYZE_MDC_AMBIGUOUS,
"under-resolved tunnel requests refinement");
expect(!coarse_faces && !coarse_vertices && !coarse_face_count &&
coarse_vertex_count == 0,
"under-resolved tunnel leaves outputs empty");
if (coarse_faces)
bu_free(coarse_faces, "coarse MDC test faces");
if (coarse_vertices)
bu_free(coarse_vertices, "coarse MDC test vertices");
}
{
int *compatibility_faces = NULL;
point_t *compatibility_vertices = NULL;
int compatibility_face_count = 0;
int compatibility_vertex_count = 0;
point_t unused_seed = VINIT_ZERO;
struct analyze_polygonize_params compatibility =
ANALYZE_POLYGONIZE_PARAMS_DEFAULT;
compatibility.max_time = 0;
compatibility.max_cycle_time = 0;
compatibility.minimum_free_mem = 0;
int status = analyze_polygonize(&compatibility_faces,
&compatibility_face_count, &compatibility_vertices,
&compatibility_vertex_count, 1.0, unused_seed, "sphere.s",
dbip, &compatibility);
expect(status == 0, "polygonize compatibility interface");
if (status == 0) {
expect(compatibility_face_count >= 4,
"compatibility contour has faces");
expect(compatibility_vertex_count >= 4,
"compatibility contour has vertices");
expect(bg_trimesh_solid2(compatibility_vertex_count,
compatibility_face_count,
reinterpret_cast<fastf_t *>(compatibility_vertices),
compatibility_faces, NULL) == 0,
"compatibility contour is a closed oriented manifold");
}
if (compatibility_faces)
bu_free(compatibility_faces, "compatibility test faces");
if (compatibility_vertices)
bu_free(compatibility_vertices, "compatibility test vertices");
compatibility.max_time = 1;
compatibility.time_offset = 1;
expect(analyze_polygonize(&compatibility_faces,
&compatibility_face_count, &compatibility_vertices,
&compatibility_vertex_count, 1.0, unused_seed, "sphere.s",
dbip, &compatibility) == 2,
"compatibility interface preserves timeout status");
expect(!compatibility_faces && !compatibility_vertices &&
!compatibility_face_count && !compatibility_vertex_count,
"compatibility timeout leaves outputs empty");
}
int *faces = NULL;
point_t *vertices = NULL;
size_t face_count = 0;
size_t vertex_count = 0;
struct analyze_mdc_params limited = ANALYZE_MDC_PARAMS_DEFAULT;
limited.min_depth = 4;
limited.max_depth = 4;
limited.max_rays = 1;
limited.minimum_free_mem = 0;
expect(analyze_mdc(&faces, &face_count, &vertices, &vertex_count,
"sphere.s", dbip, &limited) == ANALYZE_MDC_RAY_LIMIT,
"ray limit is enforced during sampling");
expect(!faces && !vertices && !face_count && !vertex_count,
"failure leaves outputs empty");
expect(analyze_mdc(NULL, &face_count, &vertices, &vertex_count,
"sphere.s", dbip, NULL) == ANALYZE_MDC_INVALID_INPUT,
"invalid output pointer is rejected");
db_close(dbip);
return failures ? 1 : 0;
}
/*
* Local Variables:
* mode: C++
* tab-width: 8
* c-basic-offset: 4
* indent-tabs-mode: t
* c-file-style: "stroustrup"
* End:
* ex: shiftwidth=4 tabstop=8
*/
+796 -134
View File
@@ -8,8 +8,8 @@
* modify it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
@@ -19,170 +19,832 @@
*/
/** @file spsr.cpp
*
* Interface to Adaptive Multigrid Solvers for Screened Poisson Surface Reconstruction
* Compatible with mkazhdan/PoissonRecon API (v18.x+).
*
* Screened Poisson surface reconstruction for fixed and adaptive oriented
* point sets.
*/
#include "common.h"
#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <cstring>
#include <exception>
#include <limits>
#include <memory>
#include <mutex>
#include <unordered_map>
#include <utility>
#include <vector>
#include "vmath.h"
#include "bu/log.h"
#include "bu/malloc.h"
#include "bg/spsr.h"
#include "RTree.h"
#if defined(__GNUC__) && !defined(__clang__)
# pragma GCC diagnostic push /* start new diagnostic pragma */
# pragma GCC diagnostic push
# pragma GCC diagnostic ignored "-Wall"
#elif defined(__clang__)
# pragma clang diagnostic push /* start new diagnostic pragma */
# pragma clang diagnostic push
# pragma clang diagnostic ignored "-Weverything"
#endif
#include "SPSR/Reconstructors.h"
#if defined(__GNUC__) && !defined(__clang__)
# pragma GCC diagnostic pop /* end ignoring warnings */
# pragma GCC diagnostic pop
#elif defined(__clang__)
# pragma clang diagnostic pop /* end ignoring warnings */
# pragma clang diagnostic pop
#endif
using namespace PoissonRecon;
// Input stream for oriented samples: implements `read` for coordinates and normals
template<typename Real, unsigned int Dim>
struct PtStream : public Reconstructor::InputOrientedSampleStream<Real, Dim>
{
PtStream(const point_t *ipnts, const vect_t *inorms, int cnt)
: _size(cnt), _current(0), _input_pnts(ipnts), _input_nrmls(inorms) {}
void reset(void) { _current = 0; }
bool read(Point<Real, Dim> &p, Point<Real, Dim> &n)
{
if (_current < _size) {
for (size_t d = 0; d < Dim; d++)
p[d] = _input_pnts[_current][d];
for (size_t d = 0; d < Dim; d++)
n[d] = _input_nrmls[_current][d];
_current++;
return true;
}
return false;
}
protected:
unsigned int _size, _current;
const point_t *_input_pnts;
const vect_t *_input_nrmls;
namespace {
using clock_type = std::chrono::steady_clock;
using real_type = fastf_t;
constexpr size_t SPSR_MAX_HINTS = 4096;
constexpr size_t SPSR_MIN_NEIGHBORS = 4;
constexpr double SPSR_NORMAL_ANGLE_DEGREES = 15.0;
constexpr double SPSR_DEDUP_FRACTION = 0.25;
constexpr int SPSR_MAX_DEPTH = 30;
struct spsr_mesh {
std::vector<int> faces;
std::vector<real_type> vertices;
};
// Output face stream: must implement write() and size()
template <typename Index>
struct PolygonStream : public Reconstructor::OutputFaceStream<2>
template<typename Real, unsigned int Dim>
struct point_stream : public Reconstructor::InputOrientedSampleStream<Real, Dim>
{
PolygonStream(std::vector<std::vector<Index>> &polygonStream) : _polygons(polygonStream) {}
size_t size(void) const { return _polygons.size(); }
point_stream(const std::vector<struct bg_3d_spsr_sample> &samples)
: current(0), input(samples)
{
}
void reset(void)
{
current = 0;
}
bool read(Point<Real, Dim> &point, Point<Real, Dim> &normal)
{
if (current >= input.size())
return false;
for (size_t dimension = 0; dimension < Dim; dimension++) {
point[dimension] = input[current].point[dimension];
normal[dimension] = input[current].normal[dimension];
}
current++;
return true;
}
private:
size_t current;
const std::vector<struct bg_3d_spsr_sample> &input;
};
template<typename Index>
struct polygon_stream : public Reconstructor::OutputFaceStream<2>
{
polygon_stream(std::vector<std::vector<Index>> &output) : polygons(output)
{
}
size_t size(void) const
{
return polygons.size();
}
size_t write(const std::vector<node_index_type> &polygon)
{
std::vector<Index> poly(polygon.size());
for (size_t i = 0; i < polygon.size(); i++) poly[i] = (Index)polygon[i];
_polygons.push_back(poly);
return _polygons.size() - 1;
std::vector<Index> converted(polygon.size());
for (size_t i = 0; i < polygon.size(); i++)
converted[i] = static_cast<Index>(polygon[i]);
polygons.push_back(std::move(converted));
return polygons.size() - 1;
}
protected:
std::vector<std::vector<Index>> &_polygons;
private:
std::vector<std::vector<Index>> &polygons;
};
// Output vertex stream: must implement write() and size()
template <typename Real, unsigned int Dim>
struct VertexStream : public Reconstructor::OutputLevelSetVertexStream<Real, Dim>
template<typename Real, unsigned int Dim>
struct vertex_stream : public Reconstructor::OutputLevelSetVertexStream<Real, Dim>
{
VertexStream(std::vector<Real> &vCoordinates) : _vCoordinates(vCoordinates) {}
size_t size(void) const { return _vCoordinates.size() / Dim; }
size_t write(const Point<Real, Dim> &p, const Point<Real, Dim> &, const Real &)
vertex_stream(std::vector<Real> &output) : coordinates(output)
{
for (unsigned int d = 0; d < Dim; d++) _vCoordinates.push_back(p[d]);
return _vCoordinates.size() / Dim - 1;
}
protected:
std::vector<Real> &_vCoordinates;
size_t size(void) const
{
return coordinates.size() / Dim;
}
size_t write(const Point<Real, Dim> &point, const Point<Real, Dim> &,
const Real &)
{
for (unsigned int dimension = 0; dimension < Dim; dimension++)
coordinates.push_back(point[dimension]);
return coordinates.size() / Dim - 1;
}
private:
std::vector<Real> &coordinates;
};
extern "C" int
bg_3d_spsr(int **faces, int *num_faces, point_t **points, int *num_pnts,
const point_t *input_points_3d, const vect_t *input_normals_3d,
int num_input_pnts, struct bg_3d_spsr_opts *spsr_opts)
static double
elapsed_seconds(const clock_type::time_point &start)
{
using Real = fastf_t;
static const unsigned int FEMSig = FEMDegreeAndBType<Reconstructor::Poisson::DefaultFEMDegree, Reconstructor::Poisson::DefaultFEMBoundary>::Signature;
using FEMSigs = IsotropicUIntPack<3, FEMSig>;
// Set up multithreading
PoissonRecon::ThreadPool::ParallelizationType = PoissonRecon::ThreadPool::ASYNC;
// Solver and extraction parameters
Reconstructor::Poisson::SolutionParameters<Real> solverParams;
solverParams.verbose = false;
solverParams.depth = (spsr_opts) ? spsr_opts->depth : 8;
solverParams.fullDepth = (spsr_opts) ? spsr_opts->full_depth : 11;
solverParams.samplesPerNode = (spsr_opts) ? spsr_opts->samples_per_node : 1.5;
solverParams.exactInterpolation = true;
Reconstructor::LevelSetExtractionParameters extractionParams;
extractionParams.forceManifold = true;
extractionParams.linearFit = false;
extractionParams.polygonMesh = false;
extractionParams.verbose = false;
PtStream<Real, 3> vstream(input_points_3d, input_normals_3d, num_input_pnts);
using Implicit = Reconstructor::Implicit<Real, 3, FEMSigs>;
using Solver = Reconstructor::Poisson::Solver<Real, 3, FEMSigs>;
Implicit *implicit = Solver::Solve(vstream, solverParams);
if (!implicit) {
bu_log("PoissonRecon: Solver::Solve failed\n");
return -1;
}
std::vector<std::vector<int>> polygons;
std::vector<Real> vCoordinates;
PolygonStream<int> pStream(polygons);
VertexStream<Real, 3> vStream(vCoordinates);
implicit->extractLevelSet(vStream, pStream, extractionParams);
*num_faces = (int)(polygons.size());
*num_pnts = (int)(vCoordinates.size() / 3);
bu_log("Point cnt: %d\n", *num_pnts);
bu_log("Face cnt: %d\n", *num_faces);
// Allocate output arrays
(*faces) = (int *)bu_calloc(*num_faces * 3, sizeof(int), "faces array");
(*points) = (point_t *)bu_calloc(*num_pnts, sizeof(point_t), "points array");
// Copy faces (triangulated output expected)
for (int i = 0; i < *num_faces; i++) {
if (polygons[i].size() != 3) {
bu_log("Warning: polygon %d is not a triangle (has %zu vertices)\n", i, polygons[i].size());
// TODO - triangulate if we don't have a triangle.
continue;
}
(*faces)[3*i+0] = polygons[i][0];
(*faces)[3*i+1] = polygons[i][1];
(*faces)[3*i+2] = polygons[i][2];
}
// Copy points
for (int i = 0; i < *num_pnts; i++) {
(*points)[i][X] = vCoordinates[3*i+0];
(*points)[i][Y] = vCoordinates[3*i+1];
(*points)[i][Z] = vCoordinates[3*i+2];
}
delete implicit;
return 0;
return std::chrono::duration<double>(clock_type::now() - start).count();
}
static bool
finite_vector(const fastf_t *vector)
{
return std::isfinite(vector[X]) && std::isfinite(vector[Y]) &&
std::isfinite(vector[Z]);
}
// Local Variables:
// tab-width: 8
// mode: C++
// c-basic-offset: 4
// indent-tabs-mode: t
// c-file-style: "stroustrup"
// End:
// ex: shiftwidth=4 tabstop=8 cino=N-s
static bool
valid_options(const struct bg_3d_spsr_opts &options)
{
if (options.degree != BG_3D_SPSR_DEFAULT_DEGREE ||
options.btype != BG_3D_SPSR_BOUNDARY_NEUMANN ||
options.max_memory_GB != BG_3D_SPSR_DEFAULT_MAX_MEM ||
options.threads != BG_3D_SPSR_DEFAULT_THREADS ||
!NEAR_ZERO(options.confidence, SMALL_FASTF) ||
!NEAR_ZERO(options.confidence_bias, SMALL_FASTF))
return false;
if (options.depth < 1 || options.depth > SPSR_MAX_DEPTH ||
options.kerneldepth < 0 || options.iterations < 0 ||
options.full_depth < 0 || options.base_depth < 0 ||
options.baseVcycles < 0 || options.samples_per_node <= 0.0 ||
options.scale < 1.0 || options.width < 0.0 ||
options.cgsolver_accuracy <= 0.0 || options.point_weight < 0.0)
return false;
return options.nonManifold >= 0 && options.nonManifold <= 1 &&
options.linearFit >= 0 && options.linearFit <= 1 &&
options.exact >= 0 && options.exact <= 1;
}
static void
set_solver_options(
Reconstructor::Poisson::SolutionParameters<real_type> &solver,
Reconstructor::LevelSetExtractionParameters &extraction,
const struct bg_3d_spsr_opts &options)
{
solver.depth = static_cast<unsigned int>(options.depth);
solver.kernelDepth = options.kerneldepth ?
static_cast<unsigned int>(options.kerneldepth) :
static_cast<unsigned int>(-1);
solver.iters = options.iterations ?
static_cast<unsigned int>(options.iterations) : 8u;
solver.fullDepth = options.full_depth ?
static_cast<unsigned int>(options.full_depth) :
std::min(5u, solver.depth);
solver.baseDepth = options.base_depth ?
static_cast<unsigned int>(options.base_depth) :
static_cast<unsigned int>(-1);
solver.baseVCycles = options.baseVcycles ?
static_cast<unsigned int>(options.baseVcycles) : 1u;
solver.samplesPerNode = options.samples_per_node;
solver.scale = options.scale;
solver.width = options.width;
solver.cgSolverAccuracy = options.cgsolver_accuracy;
solver.pointWeight = options.point_weight;
solver.exactInterpolation = options.exact != 0;
solver.confidence = false;
solver.verbose = false;
solver.showResidual = false;
extraction.forceManifold = options.nonManifold == 0;
extraction.linearFit = options.linearFit != 0;
extraction.polygonMesh = false;
extraction.verbose = false;
}
static fastf_t
sample_extent(const std::vector<struct bg_3d_spsr_sample> &samples)
{
point_t minimum;
point_t maximum;
VSETALL(minimum, INFINITY);
VSETALL(maximum, -INFINITY);
for (const auto &sample : samples)
VMINMAX(minimum, maximum, sample.point);
return DIST_PNT_PNT(minimum, maximum);
}
class sample_set
{
public:
sample_set(fastf_t tolerance, size_t limit) :
distance_tolerance(tolerance), distance_tolerance_sq(tolerance * tolerance),
point_limit(limit)
{
}
bool append(const struct bg_3d_spsr_sample &input)
{
if (!finite_vector(input.point) || !finite_vector(input.normal))
return false;
struct bg_3d_spsr_sample sample = input;
fastf_t normal_length = MAGNITUDE(sample.normal);
if (!std::isfinite(normal_length) || normal_length <= VUNITIZE_TOL)
return false;
VSCALE(sample.normal, sample.normal, 1.0 / normal_length);
double minimum[3];
double maximum[3];
for (size_t dimension = 0; dimension < 3; dimension++) {
minimum[dimension] = sample.point[dimension] - distance_tolerance;
maximum[dimension] = sample.point[dimension] + distance_tolerance;
}
bool duplicate = false;
index.Search(minimum, maximum,
[&sample, this, &duplicate](const size_t &candidate, void *) {
if (DIST_PNT_PNT_SQ(sample.point, values[candidate].point) <=
distance_tolerance_sq) {
duplicate = true;
return false;
}
return true;
}, NULL);
if (duplicate || (point_limit && values.size() >= point_limit))
return false;
size_t sample_index = values.size();
values.push_back(sample);
double point[3] = {
sample.point[X], sample.point[Y], sample.point[Z]
};
index.Insert(point, point, sample_index);
return true;
}
const std::vector<struct bg_3d_spsr_sample> &samples(void) const
{
return values;
}
bool full(void) const
{
return point_limit && values.size() >= point_limit;
}
private:
fastf_t distance_tolerance;
fastf_t distance_tolerance_sq;
size_t point_limit;
std::vector<struct bg_3d_spsr_sample> values;
RTree<size_t, double, 3> index;
};
static void
append_hint(std::vector<struct bg_3d_spsr_refinement_hint> &hints,
const point_t point, const vect_t normal, fastf_t error,
unsigned int reasons)
{
struct bg_3d_spsr_refinement_hint hint = {};
VMOVE(hint.point, point);
VMOVE(hint.normal, normal);
hint.error = error;
hint.reasons = reasons;
hints.push_back(hint);
}
template<typename Implicit>
static void
sample_hints(std::vector<struct bg_3d_spsr_refinement_hint> &hints,
const std::vector<struct bg_3d_spsr_sample> &samples,
const Implicit &implicit, fastf_t feature_size)
{
const fastf_t normal_dot_limit =
std::cos(SPSR_NORMAL_ANGLE_DEGREES * DEG2RAD);
typename Implicit::Evaluator evaluator = implicit.evaluator();
for (const auto &sample : samples) {
Point<real_type, 3> point;
for (size_t dimension = 0; dimension < 3; dimension++)
point[dimension] = sample.point[dimension];
try {
Point<real_type, 3> gradient = evaluator.grad(point);
real_type gradient_length = std::sqrt(
gradient[0] * gradient[0] + gradient[1] * gradient[1] +
gradient[2] * gradient[2]);
unsigned int reasons = 0;
fastf_t error = feature_size;
vect_t normal;
VSET(normal, gradient[0], gradient[1], gradient[2]);
if (gradient_length <= VUNITIZE_TOL) {
reasons = BG_3D_SPSR_REFINE_RESIDUAL |
BG_3D_SPSR_REFINE_NORMAL;
VSETALL(normal, 0.0);
} else {
fastf_t residual = std::fabs(
evaluator(point) - implicit.isoValue) / gradient_length;
fastf_t normal_dot = std::fabs(
VDOT(sample.normal, normal) / gradient_length);
if (residual > feature_size) {
reasons |= BG_3D_SPSR_REFINE_RESIDUAL;
error = residual;
}
if (normal_dot < normal_dot_limit) {
reasons |= BG_3D_SPSR_REFINE_NORMAL;
error = std::max(error,
feature_size * (1.0 + normal_dot_limit - normal_dot));
}
VSCALE(normal, normal, 1.0 / gradient_length);
}
if (reasons)
append_hint(hints, sample.point, normal, error, reasons);
} catch (const std::exception &) {
append_hint(hints, sample.point, sample.normal,
2.0 * feature_size, BG_3D_SPSR_REFINE_RESIDUAL);
}
}
}
static void
density_hints(std::vector<struct bg_3d_spsr_refinement_hint> &hints,
const std::vector<struct bg_3d_spsr_sample> &samples,
fastf_t feature_size)
{
RTree<size_t, double, 3> index;
for (size_t i = 0; i < samples.size(); i++) {
double point[3] = {
samples[i].point[X], samples[i].point[Y], samples[i].point[Z]
};
index.Insert(point, point, i);
}
fastf_t search_distance_sq = feature_size * feature_size;
for (size_t i = 0; i < samples.size(); i++) {
double minimum[3];
double maximum[3];
for (size_t dimension = 0; dimension < 3; dimension++) {
minimum[dimension] =
samples[i].point[dimension] - feature_size;
maximum[dimension] =
samples[i].point[dimension] + feature_size;
}
size_t neighbor_count = 0;
index.Search(minimum, maximum,
[&samples, i, search_distance_sq, &neighbor_count](
const size_t &candidate, void *) {
if (DIST_PNT_PNT_SQ(samples[i].point,
samples[candidate].point) <= search_distance_sq)
neighbor_count++;
return neighbor_count < SPSR_MIN_NEIGHBORS;
}, NULL);
if (neighbor_count < SPSR_MIN_NEIGHBORS)
append_hint(hints, samples[i].point, samples[i].normal,
feature_size, BG_3D_SPSR_REFINE_DENSITY);
}
}
static void
surface_variation_hints(
std::vector<struct bg_3d_spsr_refinement_hint> &hints,
const spsr_mesh &mesh, fastf_t feature_size)
{
size_t vertex_count = mesh.vertices.size() / 3;
std::vector<std::array<fastf_t, 3>> first_normal(vertex_count);
std::vector<fastf_t> variation(vertex_count, 0.0);
std::vector<unsigned char> have_normal(vertex_count, 0);
const fastf_t variation_limit =
1.0 - std::cos(SPSR_NORMAL_ANGLE_DEGREES * DEG2RAD);
for (size_t face = 0; face < mesh.faces.size() / 3; face++) {
int ia = mesh.faces[3 * face];
int ib = mesh.faces[3 * face + 1];
int ic = mesh.faces[3 * face + 2];
const fastf_t *a = &mesh.vertices[3 * static_cast<size_t>(ia)];
const fastf_t *b = &mesh.vertices[3 * static_cast<size_t>(ib)];
const fastf_t *c = &mesh.vertices[3 * static_cast<size_t>(ic)];
vect_t ab;
vect_t ac;
vect_t normal;
VSUB2(ab, b, a);
VSUB2(ac, c, a);
VCROSS(normal, ab, ac);
fastf_t length = MAGNITUDE(normal);
if (length <= VUNITIZE_TOL)
continue;
VSCALE(normal, normal, 1.0 / length);
const int vertices[3] = {ia, ib, ic};
for (size_t corner = 0; corner < 3; corner++) {
size_t vertex = static_cast<size_t>(vertices[corner]);
if (!have_normal[vertex]) {
VMOVE(first_normal[vertex].data(), normal);
have_normal[vertex] = 1;
} else {
variation[vertex] = std::max(variation[vertex],
1.0 - std::fabs(VDOT(first_normal[vertex].data(), normal)));
}
}
}
for (size_t vertex = 0; vertex < vertex_count; vertex++) {
if (variation[vertex] <= variation_limit)
continue;
const fastf_t *point = &mesh.vertices[3 * vertex];
append_hint(hints, point, first_normal[vertex].data(),
feature_size * (1.0 + variation[vertex]),
BG_3D_SPSR_REFINE_SURFACE_VARIATION);
}
}
static void
limit_hints(std::vector<struct bg_3d_spsr_refinement_hint> &hints,
fastf_t feature_size)
{
std::sort(hints.begin(), hints.end(),
[](const struct bg_3d_spsr_refinement_hint &left,
const struct bg_3d_spsr_refinement_hint &right) {
return left.error > right.error;
});
RTree<size_t, double, 3> locations;
std::vector<struct bg_3d_spsr_refinement_hint> selected;
selected.reserve(std::min(hints.size(), SPSR_MAX_HINTS));
fastf_t separation = feature_size * SPSR_DEDUP_FRACTION;
fastf_t separation_sq = separation * separation;
for (const auto &hint : hints) {
double minimum[3];
double maximum[3];
for (size_t dimension = 0; dimension < 3; dimension++) {
minimum[dimension] = hint.point[dimension] - separation;
maximum[dimension] = hint.point[dimension] + separation;
}
bool nearby = false;
locations.Search(minimum, maximum,
[&hint, &selected, separation_sq, &nearby](
const size_t &candidate, void *) {
if (DIST_PNT_PNT_SQ(hint.point,
selected[candidate].point) <= separation_sq) {
nearby = true;
return false;
}
return true;
}, NULL);
if (nearby)
continue;
size_t hint_index = selected.size();
selected.push_back(hint);
double point[3] = {hint.point[X], hint.point[Y], hint.point[Z]};
locations.Insert(point, point, hint_index);
if (selected.size() == SPSR_MAX_HINTS)
break;
}
hints.swap(selected);
}
template<typename Implicit>
static bool
extract_mesh(spsr_mesh &mesh, const Implicit &implicit,
const Reconstructor::LevelSetExtractionParameters &parameters)
{
std::vector<std::vector<int>> polygons;
polygon_stream<int> faces(polygons);
vertex_stream<real_type, 3> vertices(mesh.vertices);
implicit.extractLevelSet(vertices, faces, parameters);
if (mesh.vertices.empty() || mesh.vertices.size() % 3 ||
mesh.vertices.size() / 3 >
static_cast<size_t>(std::numeric_limits<int>::max()) ||
polygons.empty() ||
polygons.size() > static_cast<size_t>(std::numeric_limits<int>::max()))
return false;
mesh.faces.reserve(polygons.size() * 3);
for (const auto &polygon : polygons) {
if (polygon.size() != 3)
return false;
for (int vertex : polygon) {
if (vertex < 0 ||
static_cast<size_t>(vertex) >= mesh.vertices.size() / 3)
return false;
mesh.faces.push_back(vertex);
}
}
return true;
}
static bool
solve_once(spsr_mesh &mesh,
std::vector<struct bg_3d_spsr_refinement_hint> &hints,
const std::vector<struct bg_3d_spsr_sample> &samples,
const struct bg_3d_spsr_adaptive_opts &options)
{
static std::mutex solver_mutex;
std::lock_guard<std::mutex> guard(solver_mutex);
static const unsigned int fem_signature =
FEMDegreeAndBType<Reconstructor::Poisson::DefaultFEMDegree,
Reconstructor::Poisson::DefaultFEMBoundary>::Signature;
using fem_signatures = IsotropicUIntPack<3, fem_signature>;
using implicit_type =
Reconstructor::Implicit<real_type, 3, fem_signatures>;
using solver_type =
Reconstructor::Poisson::Solver<real_type, 3, fem_signatures>;
Reconstructor::Poisson::SolutionParameters<real_type> solver_parameters;
Reconstructor::LevelSetExtractionParameters extraction_parameters;
set_solver_options(solver_parameters, extraction_parameters,
options.solver);
ThreadPool::ParallelizationType = ThreadPool::ASYNC;
point_stream<real_type, 3> points(samples);
std::unique_ptr<implicit_type> implicit(
solver_type::Solve(points, solver_parameters));
if (!implicit || !extract_mesh(mesh, *implicit, extraction_parameters))
return false;
if (options.target_feature_size > 0.0) {
sample_hints(hints, samples, *implicit,
options.target_feature_size);
density_hints(hints, samples, options.target_feature_size);
surface_variation_hints(hints, mesh,
options.target_feature_size);
limit_hints(hints, options.target_feature_size);
}
return true;
}
static bool
copy_mesh(int **faces, int *face_count, point_t **vertices,
int *vertex_count, const spsr_mesh &mesh)
{
size_t faces_size = mesh.faces.size() * sizeof(int);
size_t vertices_size = mesh.vertices.size() * sizeof(real_type);
int *new_faces = static_cast<int *>(bu_malloc(faces_size,
"SPSR face array"));
point_t *new_vertices = static_cast<point_t *>(bu_malloc(vertices_size,
"SPSR vertex array"));
if (!new_faces || !new_vertices) {
if (new_faces)
bu_free(new_faces, "SPSR face array");
if (new_vertices)
bu_free(new_vertices, "SPSR vertex array");
return false;
}
std::memcpy(new_faces, mesh.faces.data(), faces_size);
std::memcpy(new_vertices, mesh.vertices.data(), vertices_size);
*faces = new_faces;
*face_count = static_cast<int>(mesh.faces.size() / 3);
*vertices = new_vertices;
*vertex_count = static_cast<int>(mesh.vertices.size() / 3);
return true;
}
static void
initialize_outputs(int **faces, int *face_count, point_t **vertices,
int *vertex_count, struct bg_3d_spsr_report *report)
{
if (faces)
*faces = NULL;
if (face_count)
*face_count = 0;
if (vertices)
*vertices = NULL;
if (vertex_count)
*vertex_count = 0;
if (report)
std::memset(report, 0, sizeof(*report));
}
} // namespace
extern "C" int
bg_3d_spsr_adaptive(int **faces, int *num_faces, point_t **vertices,
int *num_vertices, const struct bg_3d_spsr_sample *input_samples,
size_t sample_count, const struct bg_3d_spsr_adaptive_opts *input_options,
bg_3d_spsr_refinement_func_t refine, void *client_data,
struct bg_3d_spsr_report *report)
{
initialize_outputs(faces, num_faces, vertices, num_vertices, report);
if (!faces || !num_faces || !vertices || !num_vertices ||
!input_samples || !sample_count)
return BRLCAD_ERROR;
struct bg_3d_spsr_adaptive_opts default_options =
BG_3D_SPSR_ADAPTIVE_OPTS_DEFAULT;
const struct bg_3d_spsr_adaptive_opts &options =
input_options ? *input_options : default_options;
if (!valid_options(options.solver) ||
options.target_feature_size < 0.0 || options.max_time < 0.0 ||
(options.max_refinement_passes &&
(!refine || options.target_feature_size <= 0.0)) ||
(options.max_points && options.max_points < sample_count))
return BRLCAD_ERROR;
fastf_t extent = 0.0;
{
std::vector<struct bg_3d_spsr_sample> initial(
input_samples, input_samples + sample_count);
extent = sample_extent(initial);
}
if (!std::isfinite(extent) || extent <= SMALL_FASTF)
return BRLCAD_ERROR;
fastf_t deduplication_distance =
options.target_feature_size > 0.0 ?
options.target_feature_size * SPSR_DEDUP_FRACTION :
std::max(extent * 1.0e-12, static_cast<fastf_t>(SMALL_FASTF));
sample_set accumulated(deduplication_distance, options.max_points);
for (size_t i = 0; i < sample_count; i++)
accumulated.append(input_samples[i]);
if (accumulated.samples().empty())
return BRLCAD_ERROR;
const clock_type::time_point start = clock_type::now();
if (report)
report->initial_sample_count = accumulated.samples().size();
bool point_limit_reached = accumulated.full();
for (size_t pass = 0; ; pass++) {
if (options.max_time > 0.0 &&
elapsed_seconds(start) >= options.max_time) {
if (report)
report->termination = BG_3D_SPSR_TIME_LIMIT;
break;
}
spsr_mesh mesh;
std::vector<struct bg_3d_spsr_refinement_hint> hints;
try {
if (!solve_once(mesh, hints, accumulated.samples(), options)) {
if (report)
report->termination = BG_3D_SPSR_SOLVER_ERROR;
break;
}
} catch (const std::exception &) {
if (report)
report->termination = BG_3D_SPSR_SOLVER_ERROR;
break;
} catch (...) {
if (report)
report->termination = BG_3D_SPSR_SOLVER_ERROR;
break;
}
if (report)
report->solve_count++;
if (!refine) {
if (copy_mesh(faces, num_faces, vertices, num_vertices, mesh)) {
if (report) {
report->termination = BG_3D_SPSR_COMPLETE;
report->validation.passed = 1;
report->final_sample_count = accumulated.samples().size();
report->elapsed_time = elapsed_seconds(start);
}
return BRLCAD_OK;
}
if (report)
report->termination = BG_3D_SPSR_SOLVER_ERROR;
break;
}
double remaining_time = options.max_time > 0.0 ?
std::max(0.0, options.max_time - elapsed_seconds(start)) : 0.0;
if (options.max_time > 0.0 && remaining_time <= 0.0) {
if (report)
report->termination = BG_3D_SPSR_TIME_LIMIT;
break;
}
struct bg_3d_spsr_refinement_request request = {};
request.pass = pass;
request.target_feature_size = options.target_feature_size;
request.remaining_time = remaining_time;
request.vertices =
reinterpret_cast<const point_t *>(mesh.vertices.data());
request.vertex_count = mesh.vertices.size() / 3;
request.faces = mesh.faces.data();
request.face_count = mesh.faces.size() / 3;
request.hints = hints.data();
request.hint_count = hints.size();
struct bg_3d_spsr_refinement_response response = {};
if (refine(&response, &request, client_data) != BRLCAD_OK) {
if (report)
report->termination = BG_3D_SPSR_CALLBACK_STOP;
break;
}
if (report)
report->validation = response.validation;
if (response.validation.passed) {
if (copy_mesh(faces, num_faces, vertices, num_vertices, mesh)) {
if (report) {
report->termination = BG_3D_SPSR_COMPLETE;
report->final_sample_count = accumulated.samples().size();
report->elapsed_time = elapsed_seconds(start);
}
return BRLCAD_OK;
}
if (report)
report->termination = BG_3D_SPSR_SOLVER_ERROR;
break;
}
if (response.stop_refinement) {
if (report)
report->termination = BG_3D_SPSR_CALLBACK_STOP;
break;
}
if (pass == options.max_refinement_passes) {
if (report)
report->termination = point_limit_reached ?
BG_3D_SPSR_POINT_LIMIT : BG_3D_SPSR_CALLBACK_STOP;
break;
}
if (response.sample_count && !response.samples) {
if (report)
report->termination = BG_3D_SPSR_CALLBACK_STOP;
break;
}
if (report)
report->requested_sample_count += response.sample_count;
size_t accepted = 0;
for (size_t i = 0; i < response.sample_count; i++) {
if (accumulated.append(response.samples[i]))
accepted++;
}
if (report)
report->accepted_sample_count += accepted;
point_limit_reached = accumulated.full();
if (!accepted) {
if (report)
report->termination = point_limit_reached ?
BG_3D_SPSR_POINT_LIMIT : BG_3D_SPSR_NO_NEW_SAMPLES;
break;
}
}
if (report) {
report->final_sample_count = accumulated.samples().size();
report->elapsed_time = elapsed_seconds(start);
}
return BRLCAD_ERROR;
}
extern "C" int
bg_3d_spsr(int **faces, int *num_faces, point_t **vertices, int *num_vertices,
const point_t *input_points_3d, const vect_t *input_normals_3d,
int num_input_pnts, struct bg_3d_spsr_opts *spsr_opts)
{
if (!input_points_3d || !input_normals_3d || num_input_pnts <= 0)
return BRLCAD_ERROR;
std::vector<struct bg_3d_spsr_sample> samples(
static_cast<size_t>(num_input_pnts));
for (size_t i = 0; i < samples.size(); i++) {
VMOVE(samples[i].point, input_points_3d[i]);
VMOVE(samples[i].normal, input_normals_3d[i]);
}
struct bg_3d_spsr_adaptive_opts options =
BG_3D_SPSR_ADAPTIVE_OPTS_DEFAULT;
options.max_refinement_passes = 0;
if (spsr_opts)
options.solver = *spsr_opts;
return bg_3d_spsr_adaptive(faces, num_faces, vertices, num_vertices,
samples.data(), samples.size(), &options, NULL, NULL, NULL);
}
/*
* Local Variables:
* tab-width: 8
* mode: C++
* c-basic-offset: 4
* indent-tabs-mode: t
* c-file-style: "stroustrup"
* End:
* ex: shiftwidth=4 tabstop=8 cino=N-s
*/
+4
View File
@@ -33,6 +33,10 @@ brlcad_addexec(bg_aabb_ray aabb_ray.c "${BG_TEST_LIBS}" TEST)
brlcad_add_test(NAME bg_aabb_ray COMMAND bg_aabb_ray)
brlcad_addexec(bg_spsr spsr.cpp "${BG_TEST_LIBS}" TEST)
brlcad_add_test(NAME bg_spsr COMMAND bg_spsr)
brlcad_addexec(bg_polygon_sort polygon_sort.c "${BG_TEST_LIBS}" TEST)
brlcad_add_test(NAME bg_polygon_sort COMMAND bg_polygon_sort)
+197
View File
@@ -0,0 +1,197 @@
/* S P S R . C P P
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this file; see the file named COPYING for more
* information.
*/
#include "common.h"
#include <cmath>
#include <vector>
#include "vmath.h"
#include "bu/log.h"
#include "bu/malloc.h"
#include "bg/spsr.h"
static int failures = 0;
static void
expect(bool condition, const char *message)
{
if (condition)
return;
bu_log("FAIL: %s\n", message);
failures++;
}
static std::vector<struct bg_3d_spsr_sample>
sphere_samples(size_t count)
{
const fastf_t golden_angle = M_PI * (3.0 - std::sqrt(5.0));
std::vector<struct bg_3d_spsr_sample> samples(count);
for (size_t i = 0; i < count; i++) {
fastf_t y = 1.0 - 2.0 *
(static_cast<fastf_t>(i) + 0.5) / count;
fastf_t radius = std::sqrt(std::max(0.0, 1.0 - y * y));
fastf_t angle = golden_angle * i;
VSET(samples[i].point, radius * std::cos(angle), y,
radius * std::sin(angle));
VMOVE(samples[i].normal, samples[i].point);
}
return samples;
}
struct callback_state {
size_t calls;
size_t hints;
struct bg_3d_spsr_sample additional_sample;
bool refine_once;
bool stop;
};
static int
accept_candidate(struct bg_3d_spsr_refinement_response *response,
const struct bg_3d_spsr_refinement_request *request, void *data)
{
struct callback_state *state =
static_cast<struct callback_state *>(data);
state->calls++;
state->hints = request->hint_count;
response->validation.ray_count = 100;
if (state->stop) {
response->validation.passed = 0;
response->stop_refinement = 1;
} else if (state->refine_once && state->calls == 1) {
response->samples = &state->additional_sample;
response->sample_count = 1;
response->validation.passed = 0;
} else {
response->validation.passed = 1;
}
return BRLCAD_OK;
}
int
main(void)
{
std::vector<struct bg_3d_spsr_sample> samples = sphere_samples(256);
struct bg_3d_spsr_adaptive_opts options =
BG_3D_SPSR_ADAPTIVE_OPTS_DEFAULT;
options.solver.depth = 5;
options.max_refinement_passes = 0;
int *faces = NULL;
int face_count = 0;
point_t *vertices = NULL;
int vertex_count = 0;
struct bg_3d_spsr_report report = {};
int ret = bg_3d_spsr_adaptive(&faces, &face_count, &vertices,
&vertex_count, samples.data(), samples.size(), &options,
NULL, NULL, &report);
expect(ret == BRLCAD_OK, "fixed structured reconstruction succeeds");
expect(face_count > 0 && vertex_count > 0,
"fixed structured reconstruction returns a mesh");
expect(report.solve_count == 1 && report.validation.passed,
"fixed structured reconstruction reports one successful solve");
if (faces)
bu_free(faces, "SPSR test faces");
if (vertices)
bu_free(vertices, "SPSR test vertices");
struct callback_state state = {};
options.max_refinement_passes = 3;
options.target_feature_size = 0.1;
options.max_points = 512;
faces = NULL;
vertices = NULL;
face_count = 0;
vertex_count = 0;
ret = bg_3d_spsr_adaptive(&faces, &face_count, &vertices,
&vertex_count, samples.data(), samples.size(), &options,
accept_candidate, &state, &report);
expect(ret == BRLCAD_OK, "callback can accept an adaptive candidate");
expect(state.calls == 1, "accepted candidate stops adaptive solving");
expect(report.validation.ray_count == 100,
"adaptive report preserves callback validation");
if (faces)
bu_free(faces, "SPSR test faces");
if (vertices)
bu_free(vertices, "SPSR test vertices");
state = {};
state.stop = true;
faces = NULL;
vertices = NULL;
face_count = 0;
vertex_count = 0;
ret = bg_3d_spsr_adaptive(&faces, &face_count, &vertices,
&vertex_count, samples.data(), samples.size(), &options,
accept_candidate, &state, &report);
expect(ret == BRLCAD_ERROR,
"callback can stop an unproductive adaptive solve");
expect(state.calls == 1 && report.solve_count == 1 &&
report.termination == BG_3D_SPSR_CALLBACK_STOP,
"callback stop preserves the final validation report");
state = {};
state.refine_once = true;
VSET(state.additional_sample.point, 1.0, 0.0, 0.0);
VSET(state.additional_sample.normal, 1.0, 0.0, 0.0);
faces = NULL;
vertices = NULL;
face_count = 0;
vertex_count = 0;
ret = bg_3d_spsr_adaptive(&faces, &face_count, &vertices,
&vertex_count, samples.data(), samples.size(), &options,
accept_candidate, &state, &report);
expect(ret == BRLCAD_OK, "adaptive refinement can add a source sample");
expect(state.calls == 2 && report.solve_count == 2,
"new source sample triggers a second solve");
expect(report.accepted_sample_count == 1 &&
report.final_sample_count == samples.size() + 1,
"adaptive report counts incorporated samples");
if (faces)
bu_free(faces, "SPSR test faces");
if (vertices)
bu_free(vertices, "SPSR test vertices");
options.solver.threads = 1;
ret = bg_3d_spsr_adaptive(&faces, &face_count, &vertices,
&vertex_count, samples.data(), samples.size(), &options,
accept_candidate, &state, NULL);
expect(ret == BRLCAD_ERROR,
"unsupported solver settings are rejected");
expect(bg_3d_spsr_adaptive(NULL, &face_count, &vertices,
&vertex_count, samples.data(), samples.size(), &options,
accept_candidate, &state, NULL) == BRLCAD_ERROR,
"invalid output arguments are rejected");
return failures ? 1 : 0;
}
/*
* Local Variables:
* tab-width: 8
* mode: C++
* c-basic-offset: 4
* indent-tabs-mode: t
* c-file-style: "stroustrup"
* End:
* ex: shiftwidth=4 tabstop=8 cino=N-s
*/
+1 -1
View File
@@ -234,7 +234,7 @@ set(
mime.cmake
parallel.h
parallel_cpp11thread.cpp
process.h
process_private.h
sha1.h
struetype.h
vfont.h
+31 -17
View File
@@ -388,31 +388,45 @@ mem_sysctl(int type, size_t *memsz)
return -2;
#if defined(__FreeBSD__) && defined(HAVE_SYS_SYSCTL_H) && defined(HAVE_SYSCTL)
static const char *const page_size_name = "hw.pagesize";
static const char *const total_memory_name = "hw.physmem";
static const char *const free_pages_name = "vm.stats.vm.v_free_count";
static const char *const available_page_names[] = {
"vm.stats.vm.v_free_count",
"vm.stats.vm.v_inactive_count",
"vm.stats.vm.v_cache_count"
};
uint64_t page_size = 0;
size_t value_size = sizeof(page_size);
if (sysctlbyname(page_size_name, &page_size, &value_size, NULL, 0) != 0 ||
page_size == 0)
return -1;
if (type == BU_MEM_PAGE_SIZE)
return mem_size_from_uint64(page_size, memsz);
return mem_page_size(memsz);
uint64_t memory = 0;
value_size = sizeof(memory);
const char *memory_name = total_memory_name;
if (type == BU_MEM_AVAIL) {
memory_name = free_pages_name;
if (sysctlbyname(memory_name, &memory, &value_size, NULL, 0) != 0 ||
memory > UINT64_MAX / page_size)
return -1;
memory *= page_size;
} else if (sysctlbyname(memory_name, &memory, &value_size, NULL, 0) != 0) {
return -1;
/* FreeBSD's free queue omits clean inactive and cache pages that the VM
* can reclaim without paging another process. Counting only v_free_count
* makes an otherwise idle system appear memory-starved and needlessly
* disables memory-aware parallel work. */
for (size_t i = 0; i < sizeof(available_page_names) / sizeof(available_page_names[0]); i++) {
uint32_t pages = 0;
size_t value_size = sizeof(pages);
if (sysctlbyname(available_page_names[i], &pages, &value_size,
NULL, 0) != 0 || value_size != sizeof(pages)) {
/* v_free_count is required. Cache accounting is absent on some
* supported FreeBSD versions, so supplementary queues are optional. */
if (i == 0)
return -1;
continue;
}
if ((uint64_t)pages > UINT64_MAX - memory)
return -1;
memory += (uint64_t)pages;
}
return mem_pages_to_bytes(memory, memsz);
}
size_t value_size = sizeof(memory);
if (sysctlbyname(total_memory_name, &memory, &value_size, NULL, 0) != 0 ||
(value_size != sizeof(uint32_t) && value_size != sizeof(uint64_t)))
return -1;
return mem_size_from_uint64(memory, memsz);
#endif
return 1;
+47 -1
View File
@@ -24,6 +24,9 @@
#include <ctype.h>
#include <math.h>
#include <string.h>
#if !defined(_WIN32) || defined(__CYGWIN__)
# include <stdatomic.h>
#endif
#ifdef HAVE_SYS_TYPES_H
# include <sys/types.h>
@@ -130,6 +133,36 @@ void parallel_cpp11thread(void (*func)(int, void *), size_t ncpu, void *arg);
int BU_SEM_THREAD = BU_SEM_ID_THREAD;
#if defined(_WIN32) && !defined(__CYGWIN__)
static LONG bu_cpu_limit = 0;
static size_t
cpu_limit_get(void)
{
return (size_t)InterlockedCompareExchange(&bu_cpu_limit, 0, 0);
}
static void
cpu_limit_set(size_t ncpu)
{
(void)InterlockedExchange(&bu_cpu_limit, (LONG)ncpu);
}
#else
static atomic_size_t bu_cpu_limit = ATOMIC_VAR_INIT(0);
static size_t
cpu_limit_get(void)
{
return atomic_load_explicit(&bu_cpu_limit, memory_order_acquire);
}
static void
cpu_limit_set(size_t ncpu)
{
atomic_store_explicit(&bu_cpu_limit, ncpu, memory_order_release);
}
#endif
typedef enum {
PARALLEL_GET = 0,
@@ -355,7 +388,9 @@ bu_avail_cpus(void)
}
if (LIKELY(ncpu > 0)) {
return ncpu;
size_t cpu_limit = cpu_limit_get();
return (cpu_limit && cpu_limit < (size_t)ncpu) ? cpu_limit :
(size_t)ncpu;
}
/* non-PARALLEL */
@@ -363,6 +398,13 @@ bu_avail_cpus(void)
}
void
bu_avail_cpus_set(size_t ncpu)
{
cpu_limit_set((ncpu > MAX_PSW) ? MAX_PSW : ncpu);
}
/**********************************************************************/
@@ -538,6 +580,10 @@ parallel_interface_arg_stub(struct thread_data *user_thread_data)
void
bu_parallel(void (*func)(int, void *), size_t ncpu, void *arg)
{
size_t cpu_limit = cpu_limit_get();
if (ncpu && cpu_limit && ncpu > cpu_limit)
ncpu = cpu_limit;
#ifndef PARALLEL
if (!func)
+1 -1
View File
@@ -49,7 +49,7 @@
#include "bu/str.h"
#include "bu/datetime.h"
#include "bu/vls.h"
#include "./process.h"
#include "./process_private.h"
#ifndef HAVE_KILL
# include <TlHelp32.h>
@@ -1,4 +1,4 @@
/* P R O C E S S . H
/* P R O C E S S _ P R I V A T E . H
* BRL-CAD
*
* Copyright (c) 2013-2026 United States Government as represented by
@@ -20,8 +20,8 @@
/* Private libbu process related definitions shared between multiple files */
#ifndef LIBBU_PROCESS_H
#define LIBBU_PROCESS_H
#ifndef LIBBU_PROCESS_PRIVATE_H
#define LIBBU_PROCESS_PRIVATE_H
/* Apparently the termination code on Windows is arbitrary and somewhat
* ill-defined - see https://bugs.python.org/issue31863 for more info. Try our
@@ -29,7 +29,7 @@
* abort identification. */
#define BU_MSVC_ABORT_EXIT 97101 /* ascii - a == 97 && e == 101 */
#endif /* LIBBU_PROCESS_H */
#endif /* LIBBU_PROCESS_PRIVATE_H */
/*
* Local Variables:
+50 -3
View File
@@ -40,7 +40,7 @@
#include "bio.h"
#include "bresource.h"
#if defined(__APPLE__) && defined(HAVE_SYS_SYSCTL_H)
#if (defined(__APPLE__) || defined(__FreeBSD__)) && defined(HAVE_SYS_SYSCTL_H)
# include <sys/sysctl.h>
#endif
@@ -214,7 +214,7 @@ mem_test_failure(const char *query, ssize_t result, const size_t *output,
static int
platform_mem_tests(ssize_t all_mem)
platform_mem_tests(ssize_t all_mem, ssize_t avail_mem, ssize_t page_mem)
{
#if defined(__APPLE__) && defined(HAVE_SYS_SYSCTL_H)
uint64_t native_total = 0;
@@ -230,8 +230,55 @@ platform_mem_tests(ssize_t all_mem)
"reported %llu\n", all_mem, (unsigned long long)native_total);
return -2;
}
#elif defined(__FreeBSD__) && defined(HAVE_SYS_SYSCTL_H)
uint64_t native_total = 0;
size_t value_size = sizeof(native_total);
if (sysctlbyname("hw.physmem", &native_total, &value_size, NULL, 0) != 0 ||
(value_size != sizeof(uint32_t) && value_size != sizeof(uint64_t))) {
bu_log("bu_mem platform test: unable to query hw.physmem\n");
return -1;
}
if (native_total != (uint64_t)all_mem) {
bu_log("bu_mem platform test: BU_MEM_ALL returned %zd; hw.physmem "
"reported %llu\n", all_mem, (unsigned long long)native_total);
return -2;
}
uint64_t reclaimable_pages = 0;
const char *const page_names[] = {
"vm.stats.vm.v_free_count",
"vm.stats.vm.v_inactive_count",
"vm.stats.vm.v_cache_count"
};
for (size_t i = 0; i < sizeof(page_names) / sizeof(page_names[0]); i++) {
uint32_t pages = 0;
value_size = sizeof(pages);
if (sysctlbyname(page_names[i], &pages, &value_size, NULL, 0) != 0 ||
value_size != sizeof(pages)) {
if (i == 0)
return -3;
continue;
}
reclaimable_pages += pages;
}
/* VM counters may change between the bu_mem and direct sysctl queries.
* A one-percent allowance is ample for that race while still detecting
* the former free-pages-only implementation. */
uint64_t native_available = reclaimable_pages * (uint64_t)page_mem;
uint64_t allowance = native_total / 100u;
uint64_t available_difference = ((uint64_t)avail_mem > native_available) ?
(uint64_t)avail_mem - native_available :
native_available - (uint64_t)avail_mem;
if (available_difference > allowance) {
bu_log("bu_mem platform test: BU_MEM_AVAIL returned %zd; FreeBSD "
"reclaimable queues reported %llu\n", avail_mem,
(unsigned long long)native_available);
return -4;
}
#else
(void)all_mem;
(void)avail_mem;
(void)page_mem;
#endif
return 0;
}
@@ -445,7 +492,7 @@ main(int ac, char *av[])
if (avail_mem > all_mem)
return mem_test_failure("BU_MEM_AVAIL exceeds BU_MEM_ALL", avail_mem,
NULL, -5);
if (platform_mem_tests(all_mem) != 0)
if (platform_mem_tests(all_mem, avail_mem, page_mem) != 0)
return -6;
ssize_t process_mem = bu_mem(BU_MEM_PROCESS_AVAIL, NULL);
ssize_t resident_mem = bu_mem(BU_MEM_PROCESS_RESIDENT, NULL);
+26 -1
View File
@@ -121,6 +121,30 @@ main(int argc, char *argv[])
if (bu_getprogname()[0] == '\0')
bu_setprogname(argv[0]);
{
size_t detected_cpus = bu_avail_cpus();
size_t requested_cpus = (detected_cpus > MAX_PSW) ? MAX_PSW :
detected_cpus;
struct parallel_data limit_data = {1, NULL};
bu_avail_cpus_set(1);
if (bu_avail_cpus() != 1) {
bu_log("bu_avail_cpus process limit [FAIL]\n");
return 1;
}
memset(counter, 0, sizeof(counter));
bu_parallel(callback, requested_cpus, &limit_data);
if (tally(MAX_PSW) != 1) {
bu_log("bu_parallel process limit [FAIL]\n");
return 1;
}
bu_avail_cpus_set(0);
if (bu_avail_cpus() != detected_cpus) {
bu_log("bu_avail_cpus process limit reset [FAIL]\n");
return 1;
}
bu_log("bu_avail_cpus and bu_parallel process limit [PASS]\n");
}
while ((c = bu_getopt(argc, argv, "P:!:")) != -1) {
switch (c) {
case 'P':
@@ -137,8 +161,9 @@ main(int argc, char *argv[])
}
/* test calling without a hook function */
memset(counter, 0, sizeof(counter));
bu_parallel(NULL, ncpu, NULL);
if (tally(ncpu) != 0) {
if (tally(MAX_PSW) != 0) {
bu_log("bu_parallel zero callback [FAIL]\n");
return 1;
}
+9 -13
View File
@@ -3,16 +3,21 @@ brlcad_find_package(manifold REQUIRED)
set(FACETIZE_LIBS libged libbu manifold::manifold)
add_subdirectory(subprocess)
add_subdirectory(tests)
set(
FACETIZE_SRCS
brep.cpp
facetize.cpp
nmg.cpp
orchestrator.cpp
plan.cpp
process.cpp
regions.cpp
transfer.cpp
tri.cpp
util.cpp
validation.cpp
worker.cpp
)
@@ -26,23 +31,14 @@ ged_plugin_library(ged-facetize
# Plugin requires subprocess to work
add_dependencies(ged-facetize ged_exec facetize_process)
# Old version of facetize
set(OLD_FACETIZE_SRCS
old_facetize.cpp
old_facetize_log.c
)
ged_plugin_library(ged-old_facetize
${OLD_FACETIZE_SRCS}
INCLUDES ${CMAKE_CURRENT_SOURCE_DIR}
LIBS ${FACETIZE_LIBS}
)
cmakefiles(
CMakeLists.txt
tess_opts.h
ged_facetize.h
old_facetize.h
orchestrator.h
process.h
transfer.h
validation.h
worker.h
TODO
)
+40 -28
View File
@@ -35,26 +35,27 @@
#include "./ged_facetize.h"
int
_nonovlp_brep_facetize(struct _ged_facetize_state *s, int argc, const char **argv)
_nonovlp_brep_facetize(struct _ged_facetize_state *s, const FacetizePlan &plan)
{
int newobj_cnt;
struct directory **dpa = NULL;
RT_CHECK_DBI(s->dbip);
if (argc <= 0) return BRLCAD_ERROR;
dpa = (struct directory **)bu_calloc(argc, sizeof(struct directory *), "dp array");
newobj_cnt = _ged_sort_existing_objs(s->dbip, argc, argv, dpa);
if (_ged_validate_objs_list(s, argc, argv, newobj_cnt) == BRLCAD_ERROR) {
bu_free(dpa, "dp array");
return BRLCAD_ERROR;
int argc = (int)plan.inputs.size();
std::vector<const char *> argv = plan.input_argv();
struct directory **dpa = (struct directory **)bu_calloc(argc,
sizeof(struct directory *), "BRep input directory array");
for (int i = 0; i < argc; i++) {
dpa[i] = db_lookup(s->dbip, argv[i], LOOKUP_QUIET);
if (!dpa[i]) {
facetize_failure(s, "BRep input object '%s' disappeared before processing", argv[i]);
bu_free(dpa, "BRep input directory array");
return BRLCAD_ERROR;
}
}
/* If anything specified has subtractions or intersections, we can't facetize it with
* this logic - that would require all-up Boolean evaluation processing. */
const char *non_union = "-bool + -or -bool -";
if (db_search(NULL, DB_SEARCH_QUIET, non_union, newobj_cnt, dpa, s->dbip, NULL, NULL, NULL) > 0) {
if (db_search(NULL, DB_SEARCH_QUIET, non_union, argc, dpa, s->dbip, NULL, NULL, NULL) > 0) {
bu_free(dpa, "dp array");
bu_vls_printf(s->gedp->ged_result_str, "Found intersection or subtraction objects in specified inputs - currently unsupported. Aborting.\n");
return BRLCAD_ERROR;
@@ -63,7 +64,7 @@ _nonovlp_brep_facetize(struct _ged_facetize_state *s, int argc, const char **arg
/* If anything other than combs or breps exists in the specified inputs, we can't
* process with this logic - requires a preliminary brep conversion. */
const char *obj_types = "! -type c -and ! -type brep";
if (db_search(NULL, DB_SEARCH_QUIET, obj_types, newobj_cnt, dpa, s->dbip, NULL, NULL, NULL) > 0) {
if (db_search(NULL, DB_SEARCH_QUIET, obj_types, argc, dpa, s->dbip, NULL, NULL, NULL) > 0) {
bu_free(dpa, "dp array");
bu_vls_printf(s->gedp->ged_result_str, "Found objects in specified inputs which are not of type comb or brep- currently unsupported. Aborting.\n");
return BRLCAD_ERROR;
@@ -83,12 +84,17 @@ _nonovlp_brep_facetize(struct _ged_facetize_state *s, int argc, const char **arg
bu_free(dpa, "dp array");
return BRLCAD_ERROR;
}
for (int i = 0; i < newobj_cnt; i++) {
int xac = 3;
for (int i = 0; i < argc; i++) {
int xac = 2;
const char *xav[3] = {NULL};
xav[0] = "xpush";
xav[1] = dpa[i]->d_namep;
ged_exec_xpush(wgedp, xac, (const char **)xav);
if (ged_exec_xpush(wgedp, xac, (const char **)xav) != BRLCAD_OK) {
facetize_failure(s, "xpush failed for BRep input '%s'", dpa[i]->d_namep);
ged_close(wgedp);
bu_free(dpa, "BRep input directory array");
return BRLCAD_ERROR;
}
}
/* Used the libged tolerances */
@@ -103,7 +109,7 @@ _nonovlp_brep_facetize(struct _ged_facetize_state *s, int argc, const char **arg
const char *active_breps = "-type brep";
struct bu_ptbl *br;
BU_ALLOC(br, struct bu_ptbl);
if (db_search(br, DB_SEARCH_RETURN_UNIQ_DP, active_breps, newobj_cnt, dpa, wgedp->dbip, NULL, NULL, NULL) < 0) {
if (db_search(br, DB_SEARCH_RETURN_UNIQ_DP, active_breps, argc, dpa, wgedp->dbip, NULL, NULL, NULL) < 0) {
bu_free(dpa, "dp array");
bu_free(br, "brep results");
return BRLCAD_ERROR;
@@ -210,13 +216,6 @@ _nonovlp_brep_facetize(struct _ged_facetize_state *s, int argc, const char **arg
}
}
/* Done changing stuff in working database. */
ged_close(wgedp);
for (size_t i = 0; i < ss_cdt.size(); i++) {
ON_Brep_CDT_Destroy(ss_cdt[i]);
}
/* Keep out just what we asked for into a .g file */
struct bu_vls kwfile = BU_VLS_INIT_ZERO;
bu_vls_sprintf(&kwfile, "%s_keep", bu_vls_cstr(s->wfile));
@@ -227,7 +226,21 @@ _nonovlp_brep_facetize(struct _ged_facetize_state *s, int argc, const char **arg
av[i+2] = argv[i];
}
av[argc+2] = NULL;
ged_exec_keep(wgedp, argc+2, av);
if (ged_exec_keep(wgedp, argc+2, av) != BRLCAD_OK) {
facetize_failure(s, "unable to stage processed BRep hierarchy");
bu_free(av, "av");
bu_vls_free(&kwfile);
ged_close(wgedp);
for (size_t i = 0; i < ss_cdt.size(); i++)
ON_Brep_CDT_Destroy(ss_cdt[i]);
return BRLCAD_ERROR;
}
/* Done changing stuff in working database. */
ged_close(wgedp);
for (size_t i = 0; i < ss_cdt.size(); i++)
ON_Brep_CDT_Destroy(ss_cdt[i]);
/* Merge working geometry into original file */
av[0] = "dbconcat";
@@ -235,14 +248,14 @@ _nonovlp_brep_facetize(struct _ged_facetize_state *s, int argc, const char **arg
av[2] = bu_vls_cstr(&kwfile);
av[3] = "brep_facetize_"; // TODO - customize
av[4] = NULL;
ged_exec_dbconcat(s->gedp, 4, av);
int concat_ret = ged_exec_dbconcat(s->gedp, 4, av);
bu_free(av, "av");
bu_vls_free(&kwfile);
/* Clean up */
bu_dirclear(s->wdir);
return BRLCAD_OK;
return (concat_ret == BRLCAD_OK) ? BRLCAD_OK : BRLCAD_ERROR;
}
// Local Variables:
@@ -253,4 +266,3 @@ _nonovlp_brep_facetize(struct _ged_facetize_state *s, int argc, const char **arg
// c-file-style: "stroustrup"
// End:
// ex: shiftwidth=4 tabstop=8
+198 -297
View File
@@ -32,6 +32,7 @@
#include <vector>
#include "bu/app.h"
#include "bu/file.h"
#include "bu/path.h"
#include "bu/opt.h"
#include "wdb.h"
@@ -42,6 +43,8 @@
#include "./tess_opts.h"
#include "./ged_facetize.h"
static const char FACETIZE_NMG_OUTPUT_FILE_SUFFIX[] = "_nmg_outputs.g";
static int
facetize_process_output(struct bu_vls *output, const char **command)
{
@@ -98,223 +101,132 @@ _facetize_methods_help(struct ged *gedp)
bu_vls_free(&method_output);
}
struct _ged_facetize_state *
_ged_facetize_state_create()
static int
facetize_commit_nmg_outputs(struct _ged_facetize_state *s,
const std::vector<std::string> &output_names)
{
struct _ged_facetize_state *s = NULL;
BU_GET(s, struct _ged_facetize_state);
s->verbosity = 0;
s->no_empty = 0;
s->make_nmg = 0;
s->nonovlp_brep = 0;
s->no_fixup = 0;
s->nmg_booleval = 0;
s->use_variant_plan = 1;
s->tolerate_failures = 0;
s->tolerated_failures = 0;
s->tolerated_failure_details = 0;
s->tolerated_failure_omitted = 0;
s->inspection_regions = 0;
s->perturb_sa_tol = 10.0;
s->perturb_vol_tol = 10.0;
if (!s || output_names.empty())
return BRLCAD_ERROR;
s->wdir = NULL;
BU_GET(s->log_file, struct bu_vls);
bu_vls_init(s->log_file);
s->log_file_is_temporary = 0;
s->lfile = NULL;
BU_GET(s->failure_msg, struct bu_vls);
bu_vls_init(s->failure_msg);
BU_GET(s->tolerated_failure_log, struct bu_vls);
bu_vls_init(s->tolerated_failure_log);
BU_GET(s->region_summary, struct bu_vls);
bu_vls_init(s->region_summary);
BU_GET(s->primitive_summary, struct bu_vls);
bu_vls_init(s->primitive_summary);
BU_GET(s->inspection_log, struct bu_vls);
bu_vls_init(s->inspection_log);
BU_GET(s->wfile, struct bu_vls);
bu_vls_init(s->wfile);
BU_GET(s->bname, struct bu_vls);
bu_vls_init(s->bname);
s->regions = 0;
s->resume = 0;
s->in_place = 0;
BU_GET(s->suffix, struct bu_vls);
bu_vls_init(s->suffix);
BU_GET(s->prefix, struct bu_vls);
bu_vls_init(s->prefix);
BU_GET(s->solid_suffix, struct bu_vls);
bu_vls_init(s->solid_suffix);
bu_vls_sprintf(s->solid_suffix, ".bot");
s->max_time = 0;
s->max_pnts = 0;
s->tol = NULL;
s->nonovlp_threshold = 0;
s->gedp = NULL;
s->write_profiled = 0;
s->write_profile_bytes = 0.0;
s->write_profile_usec = 0.0;
s->variant_plan = NULL;
return s;
}
void _ged_facetize_state_destroy(struct _ged_facetize_state *s)
{
if (!s)
return;
if (s->wdir)
bu_free(s->wdir, "wdir");
if (s->bname) {
bu_vls_free(s->bname);
BU_PUT(s->bname, struct bu_vls);
}
if (s->lfile) {
fclose(s->lfile);
s->lfile = NULL;
}
if (s->log_file) {
bu_vls_free(s->log_file);
BU_PUT(s->log_file, struct bu_vls);
}
if (s->failure_msg) {
bu_vls_free(s->failure_msg);
BU_PUT(s->failure_msg, struct bu_vls);
}
if (s->tolerated_failure_log) {
bu_vls_free(s->tolerated_failure_log);
BU_PUT(s->tolerated_failure_log, struct bu_vls);
}
if (s->region_summary) {
bu_vls_free(s->region_summary);
BU_PUT(s->region_summary, struct bu_vls);
}
if (s->primitive_summary) {
bu_vls_free(s->primitive_summary);
BU_PUT(s->primitive_summary, struct bu_vls);
}
if (s->inspection_log) {
bu_vls_free(s->inspection_log);
BU_PUT(s->inspection_log, struct bu_vls);
}
if (s->wfile) {
bu_vls_free(s->wfile);
BU_PUT(s->wfile, struct bu_vls);
}
if (s->prefix) {
bu_vls_free(s->prefix);
BU_PUT(s->prefix, struct bu_vls);
}
if (s->suffix) {
bu_vls_free(s->suffix);
BU_PUT(s->suffix, struct bu_vls);
}
if (s->solid_suffix) {
bu_vls_free(s->solid_suffix);
BU_PUT(s->solid_suffix, struct bu_vls);
}
if (s->variant_plan) {
delete (FacetizeVariantPlan *)s->variant_plan;
s->variant_plan = NULL;
}
BU_PUT(s, struct _ged_facetize_state);
}
int
_ged_facetize_objs(struct _ged_facetize_state *s, int argc, const char **argv)
{
int ret = BRLCAD_ERROR;
int newobj_cnt, i;
int ok_cnt = 0;
const char *oname = NULL;
const char *av[2];
struct directory **dpa = NULL;
struct directory *idpa[2];
struct db_i *dbip = s->dbip;
RT_CHECK_DBI(dbip);
if (argc < 0) return BRLCAD_ERROR;
dpa = (struct directory **)bu_calloc(argc, sizeof(struct directory *), "dp array");
newobj_cnt = _ged_sort_existing_objs(dbip, argc, argv, dpa);
if (_ged_validate_objs_list(s, argc, argv, newobj_cnt) == BRLCAD_ERROR) {
bu_free(dpa, "dp array");
std::string keep_file = std::string(bu_vls_cstr(s->wfile)) +
FACETIZE_NMG_OUTPUT_FILE_SUFFIX;
(void)bu_file_delete(keep_file.c_str());
struct ged *working_gedp = ged_open("db", bu_vls_cstr(s->wfile), 1);
if (!working_gedp) {
facetize_failure(s, "unable to open staged NMG Boolean database '%s'",
bu_vls_cstr(s->wfile));
return BRLCAD_ERROR;
}
if (!s->in_place) {
oname = argv[argc-1];
argc--;
std::vector<const char *> keep_argv;
keep_argv.reserve(output_names.size() + 2);
keep_argv.push_back("keep");
keep_argv.push_back(keep_file.c_str());
for (const std::string &output_name : output_names)
keep_argv.push_back(output_name.c_str());
int keep_result = ged_exec_keep(working_gedp, (int)keep_argv.size(),
keep_argv.data());
ged_close(working_gedp);
if (keep_result != BRLCAD_OK) {
facetize_failure(s, "unable to stage NMG Boolean outputs for import");
(void)bu_file_delete(keep_file.c_str());
return BRLCAD_ERROR;
}
/* If we're doing an NMG output, or we have been instructed to, use the
* old-school libnmg booleval */
if (s->make_nmg || s->nmg_booleval) {
if (!s->in_place) {
ret = _ged_facetize_nmgeval(s, argc, argv, oname);
goto booleval_cleanup;
} else {
for (i = 0; i < argc; i++) {
av[0] = argv[i];
av[1] = NULL;
ret = _ged_facetize_nmgeval(s, 1, av, av[0]);
if (ret == BRLCAD_ERROR && s->tolerate_failures) {
facetize_tolerated_failure(s, "object '%s' failed during NMG boolean evaluation and was skipped", argv[i]);
continue;
}
if (ret == BRLCAD_ERROR)
goto booleval_cleanup;
ok_cnt++;
}
if (s->tolerate_failures && ok_cnt > 0)
ret = BRLCAD_OK;
goto booleval_cleanup;
const char *concat_argv[] = {"dbconcat", "-O", keep_file.c_str(), NULL};
int concat_result = ged_exec_dbconcat(s->gedp, 3, concat_argv);
(void)bu_file_delete(keep_file.c_str());
if (concat_result != BRLCAD_OK) {
facetize_failure(s, "unable to import staged NMG Boolean outputs");
return BRLCAD_ERROR;
}
bu_vls_trunc(s->gedp->ged_result_str, 0);
db_update_nref(s->dbip);
return BRLCAD_OK;
}
int
_ged_facetize_objs(struct _ged_facetize_state *s, const FacetizePlan &plan)
{
int ret = BRLCAD_ERROR;
int ok_cnt = 0;
struct db_i *dbip = s->dbip;
RT_CHECK_DBI(dbip);
int argc = (int)plan.inputs.size();
std::vector<const char *> argv = plan.input_argv();
struct directory **dpa = (struct directory **)bu_calloc(argc,
sizeof(struct directory *), "facetize input directory array");
for (int i = 0; i < argc; i++) {
dpa[i] = db_lookup(dbip, argv[i], LOOKUP_QUIET);
if (!dpa[i]) {
facetize_failure(s, "input object '%s' disappeared before evaluation", argv[i]);
bu_free(dpa, "facetize input directory array");
return BRLCAD_ERROR;
}
}
// If we're not doing NMG, use the Manifold booleval
if (!s->in_place) {
ret = _ged_facetize_booleval(s, argc, dpa, oname, false, false);
const char *output_name = plan.execution.writes_in_place() ?
NULL : plan.output.c_str();
if (plan.execution.uses_nmg_boolean()) {
if (_ged_facetize_working_file_setup(s, NULL) != BRLCAD_OK) {
facetize_failure(s,
"unable to prepare the NMG Boolean working database");
goto booleval_cleanup;
}
struct db_i *work_dbip = db_open(bu_vls_cstr(s->wfile),
DB_OPEN_READWRITE);
if (!work_dbip || db_dirbuild(work_dbip) < 0) {
if (work_dbip)
db_close(work_dbip);
facetize_failure(s,
"unable to open the NMG Boolean working database");
goto booleval_cleanup;
}
std::vector<std::string> successful_outputs;
if (!plan.execution.writes_in_place()) {
ret = _ged_facetize_nmgeval(s, work_dbip,
s->dbip->dbi_filename, plan.inputs, output_name);
if (ret == BRLCAD_OK)
successful_outputs.emplace_back(output_name);
} else {
for (int i = 0; i < argc; i++) {
std::vector<std::string> object_input(1, plan.inputs[i]);
ret = _ged_facetize_nmgeval(s, work_dbip,
s->dbip->dbi_filename, object_input, argv[i]);
if (ret == BRLCAD_ERROR && s->tolerate_failures) {
facetize_tolerated_failure(s,
"object '%s' failed during NMG boolean evaluation and was skipped",
argv[i]);
continue;
}
if (ret == BRLCAD_ERROR)
break;
successful_outputs.emplace_back(argv[i]);
}
if (s->tolerate_failures && !successful_outputs.empty())
ret = BRLCAD_OK;
}
db_close(work_dbip);
if (ret == BRLCAD_OK)
ret = facetize_commit_nmg_outputs(s, successful_outputs);
if (ret == BRLCAD_OK)
bu_dirclear(s->wdir);
goto booleval_cleanup;
}
if (!plan.execution.writes_in_place()) {
ret = _ged_facetize_booleval(s, argc, dpa, output_name, false, false);
} else {
for (i = 0; i < argc; i++) {
idpa[0] = dpa[i];
idpa[1] = NULL;
ret = _ged_facetize_booleval(s, 1, (struct directory **)idpa, argv[i], false, false);
for (int i = 0; i < argc; i++) {
struct directory *object_dpa[] = {dpa[i], NULL};
ret = _ged_facetize_booleval(s, 1, object_dpa, argv[i], false, false);
if (ret == BRLCAD_ERROR && s->tolerate_failures) {
facetize_tolerated_failure(s, "object '%s' failed during BoT boolean evaluation and was skipped", argv[i]);
facetize_tolerated_failure(s,
"object '%s' failed during BoT boolean evaluation and was skipped",
argv[i]);
continue;
}
if (ret == BRLCAD_ERROR)
@@ -332,7 +244,7 @@ _ged_facetize_objs(struct _ged_facetize_state *s, int argc, const char **argv)
bu_dirclear(s->wdir);
booleval_cleanup:
bu_free(dpa, "dp array");
bu_free(dpa, "facetize input directory array");
return ret;
}
@@ -348,22 +260,32 @@ ged_facetize_core(struct ged *gedp, int argc, const char *argv[])
long verbosity = 0;
int force_perturb = 0;
int disable_perturb = 0;
method_options_t *method_options = new method_options_t;
struct _ged_facetize_state *s = _ged_facetize_state_create();
s->gedp = gedp;
s->dbip = gedp->dbip;
s->method_opts = method_options;
int nmg_output = 0;
int region_mode = 0;
int in_place = 0;
int nmg_boolean = 0;
int nonoverlap_brep = 0;
bool use_perturbation = false;
GED_CHECK_DATABASE_OPEN(gedp, BRLCAD_ERROR);
GED_CHECK_READ_ONLY(gedp, BRLCAD_ERROR);
GED_CHECK_ARGC_GT_0(gedp, argc, BRLCAD_ERROR);
FacetizeSession session(gedp);
struct _ged_facetize_state *s = session.state();
method_options_t *method_options = session.method_options();
FacetizePlan plan;
/* General options */
struct bu_opt_desc d[25];
struct bu_opt_desc d[26];
BU_OPT(d[ 0], "h", "help", "", NULL, &print_help, "Print help and exit");
BU_OPT(d[ 1], "v", "verbose", "", &bu_opt_incr_long, &verbosity, "Verbose output (multiple flags increase verbosity)");
BU_OPT(d[ 2], "q", "quiet", "", NULL, &quiet, "Suppress all output (overrides verbose flag)");
BU_OPT(d[ 3], "n", "nmg-output", "", NULL, &(s->make_nmg), "Create an N-Manifold Geometry (NMG) object (default is to create a triangular BoT mesh). Note that this will disable most other processing options and may reduce the conversion success rate.");
BU_OPT(d[ 4], "r", "regions", "", NULL, &(s->regions), "For combs, walk the trees and create new copies of the hierarchies with each region's CSG tree replaced by a facetized evaluation of that region. By default, enables perturb methodology (can be disabled - see --no-perturb)");
BU_OPT(d[ 3], "n", "nmg-output", "", NULL, &nmg_output, "Create an N-Manifold Geometry (NMG) object (default is to create a triangular BoT mesh). Note that this will disable most other processing options and may reduce the conversion success rate.");
BU_OPT(d[ 4], "r", "regions", "", NULL, &region_mode, "For combs, walk the trees and create new copies of the hierarchies with each region's CSG tree replaced by a facetized evaluation of that region. By default, enables perturb methodology (can be disabled - see --no-perturb)");
BU_OPT(d[ 5], "s", "suffix", "<str>", &bu_opt_vls, s->suffix, "When creating new objects for facetize outputs, use this suffix to avoid conflicts");
BU_OPT(d[ 6], "p", "prefix", "<str>", &bu_opt_vls, s->prefix, "When creating new objects for facetize, use this prefix to avoid conflicts");
BU_OPT(d[ 7], "", "in-place", "", NULL, &(s->in_place), "Replace the specified object(s) with their facetizations. (Warning: this option changes pre-existing geometry!)");
BU_OPT(d[ 7], "", "in-place", "", NULL, &in_place, "Replace the specified object(s) with their facetizations. (Warning: this option changes pre-existing geometry!)");
BU_OPT(d[ 8], "", "max-time", "#", &bu_opt_int, &(s->max_time), "Maximum time to spend per object (in seconds). Default is method specific. Note that specifying shorter times may cut off conversions (particularly using sampling methods) that could succeed with longer runtimes. Per-method time limits can also be adjusted to allow longer runtimes on slower methods.");
BU_OPT(d[ 9], "", "max-pnts", "#", &bu_opt_int, &(s->max_pnts), "Maximum number of pnts per object to use when applying ray sampling methods.");
BU_OPT(d[10], "", "resume", "", NULL, &(s->resume), "Resume an interrupted conversion");
@@ -371,20 +293,17 @@ ged_facetize_core(struct ged *gedp, int argc, const char *argv[])
BU_OPT(d[12], "", "method-opts", "METHOD opt1=val opt2=val...", &_tess_method_opts, method_options, "For the specified method, set the specified options.");
BU_OPT(d[13], "", "no-empty", "", NULL, &(s->no_empty), "Do not output empty BoT objects if the boolean evaluation results in an empty solid.");
BU_OPT(d[14], "", "log-file", "<filename>", &bu_opt_vls, s->log_file, "Specify a location to use for the log file.");
BU_OPT(d[15], "", "nmg-booleval", "", NULL, &s->nmg_booleval, "Use libnmg Boolean evaluation algorithm, even if we're producing a BoT. Less robust, but if it succeeds it may produce cleaner output for coplanar inputs.");
BU_OPT(d[15], "", "nmg-booleval", "", NULL, &nmg_boolean, "Use libnmg Boolean evaluation algorithm, even if we're producing a BoT. Less robust, but if it succeeds it may produce cleaner output for coplanar inputs.");
BU_OPT(d[16], "", "disable-fixup", "", NULL, &s->no_fixup, "Disable post-processing steps intended to improve generated meshes.");
BU_OPT(d[17], "", "perturb", "", NULL, &force_perturb, "Enable the coplanarity-avoidance perturbation step (overrides non -r option default, conflicts with --no-perturb).");
BU_OPT(d[18], "", "no-perturb", "", NULL, &disable_perturb, "Disable the coplanarity-avoidance perturbation step (overrides -r option default, conflicts with --perturb).");
BU_OPT(d[19], "B", "", "", NULL, &s->nonovlp_brep, "EXPERIMENTAL: non-overlapping facetization to BoT objects of union-only brep comb tree.");
BU_OPT(d[19], "B", "", "", NULL, &nonoverlap_brep, "EXPERIMENTAL: non-overlapping facetization to BoT objects of union-only brep comb tree.");
BU_OPT(d[20], "t", "threshold", "#", &bu_opt_fastf_t, &s->nonovlp_threshold, "EXPERIMENTAL: max ovlp threshold length for -B mode.");
BU_OPT(d[21], "", "perturb-sa-tol", "#", &bu_opt_fastf_t, &s->perturb_sa_tol, "Surface-area percentage threshold (0-100) that triggers the coplanarity-avoidance perturb retry when the CSG Crofton SA differs from the BoT SA by more than this amount. Default is 10.");
BU_OPT(d[22], "", "perturb-vol-tol", "#", &bu_opt_fastf_t, &s->perturb_vol_tol, "Volume percentage threshold (0-100) that triggers the coplanarity-avoidance perturb retry when the CSG Crofton volume differs from the BoT volume by more than this amount. Default is 10.");
BU_OPT(d[23], "", "tolerate-failures", "", NULL, &s->tolerate_failures, "Continue after failed primitive or subtree evaluations and generate a partial result. The output will not be a complete representation of the input if any failures are tolerated.");
BU_OPT_NULL(d[24]);
GED_CHECK_DATABASE_OPEN(gedp, BRLCAD_ERROR);
GED_CHECK_READ_ONLY(gedp, BRLCAD_ERROR);
GED_CHECK_ARGC_GT_0(gedp, argc, BRLCAD_ERROR);
BU_OPT(d[24], "j", "jobs", "#", &bu_opt_int, &s->max_workers, "Maximum number of facetize worker processes. The default is selected conservatively from CPU and memory availability.");
BU_OPT_NULL(d[25]);
/* skip command name argv[0] */
argc-=(argc>0); argv+=(argc>0);
@@ -399,7 +318,7 @@ ged_facetize_core(struct ged *gedp, int argc, const char *argv[])
bu_vls_printf(gedp->ged_result_str, "option parsing failed: %s\n", bu_vls_cstr(&omsg));
ret = BRLCAD_ERROR;
bu_vls_free(&omsg);
goto ged_facetize_memfree;
goto ged_facetize_done;
}
bu_vls_free(&omsg);
@@ -407,17 +326,49 @@ ged_facetize_core(struct ged *gedp, int argc, const char *argv[])
if (force_perturb && disable_perturb) {
bu_vls_printf(gedp->ged_result_str, "Can only specify one of --perturb or --no-perturb\n");
ret = BRLCAD_ERROR;
goto ged_facetize_memfree;
goto ged_facetize_done;
}
if (s->max_workers < 0 || s->max_workers > MAX_PSW) {
bu_vls_printf(gedp->ged_result_str,
"--jobs must be between 0 and %d\n", MAX_PSW);
ret = BRLCAD_ERROR;
goto ged_facetize_done;
}
if (s->max_time < 0) {
bu_vls_printf(gedp->ged_result_str,
"--max-time must be greater than or equal to 0\n");
ret = BRLCAD_ERROR;
goto ged_facetize_done;
}
if (s->max_pnts < 0) {
bu_vls_printf(gedp->ged_result_str,
"--max-pnts must be greater than or equal to 0\n");
ret = BRLCAD_ERROR;
goto ged_facetize_done;
}
// Set no_perturb according to options. Enable by default for -r, disable
// if not doing -r (one large BoT, generally a less likely candidate for
// worrying about volume/surf_area matching.)
s->no_perturb = (s->regions) ? 0 : 1;
if (region_mode && nonoverlap_brep) {
bu_vls_printf(gedp->ged_result_str,
"--regions and -B select different processing scopes and cannot be combined\n");
ret = BRLCAD_ERROR;
goto ged_facetize_done;
}
s->execution.output_format = nmg_output ?
FacetizeOutputFormat::Nmg : FacetizeOutputFormat::Bot;
s->execution.boolean_engine = (nmg_output || nmg_boolean) ?
FacetizeBooleanEngine::Nmg : FacetizeBooleanEngine::Manifold;
s->execution.scope = nonoverlap_brep ? FacetizeScope::NonOverlappingBrep :
(region_mode ? FacetizeScope::Regions : FacetizeScope::Objects);
s->execution.commit_mode = in_place ?
FacetizeCommitMode::InPlace : FacetizeCommitMode::NewObject;
use_perturbation = region_mode;
if (disable_perturb)
s->no_perturb = 1;
use_perturbation = false;
if (force_perturb)
s->no_perturb = 0;
use_perturbation = true;
s->execution.perturb_mode = use_perturbation ?
FacetizePerturbMode::Enabled : FacetizePerturbMode::Disabled;
s->verbosity = (int)verbosity;
@@ -442,88 +393,38 @@ ged_facetize_core(struct ged *gedp, int argc, const char *argv[])
s->verbosity = -1;
/* Don't allow incorrect type suffixes */
if (s->make_nmg && BU_STR_EQUAL(bu_vls_cstr(s->solid_suffix), ".bot")) {
if (s->execution.writes_nmg() && BU_STR_EQUAL(bu_vls_cstr(s->solid_suffix), ".bot")) {
bu_vls_sprintf(s->solid_suffix, ".nmg");
}
if (!s->make_nmg && BU_STR_EQUAL(bu_vls_cstr(s->solid_suffix), ".nmg")) {
if (!s->execution.writes_nmg() && BU_STR_EQUAL(bu_vls_cstr(s->solid_suffix), ".nmg")) {
bu_vls_sprintf(s->solid_suffix, ".bot");
}
/* Check if we want/need help */
need_help += (argc < 1);
need_help += (argc < 2 && !s->in_place && !s->regions && !s->resume && !s->nonovlp_brep);
if (print_help || need_help || argc < 1) {
need_help = argc < (s->execution.writes_in_place() ? 1 : 2);
if (print_help || need_help) {
_ged_cmd_help(gedp, usage, d);
_facetize_methods_help(gedp);
ret = (need_help) ? BRLCAD_ERROR : BRLCAD_OK;
goto ged_facetize_memfree;
goto ged_facetize_done;
}
/* Beyond this point, we're likely to need info on the cache directory. Generate some
* paths and strings we will need. */
{
// Get the root filename
char rfname[MAXPATHLEN];
bu_file_realpath(gedp->dbip->dbi_filename, rfname);
bu_path_component(s->bname, rfname, BU_PATH_BASENAME);
// Hash the path string and construct a location in the cache directory
unsigned long long hash_num = bu_data_hash((void *)bu_vls_cstr(s->bname), bu_vls_strlen(s->bname));
struct bu_vls dname = BU_VLS_INIT_ZERO;
bu_vls_sprintf(&dname, "facetize_%llu", hash_num);
s->wdir = (char *)bu_calloc(MAXPATHLEN, sizeof(char), "wdir");
bu_dir(s->wdir, MAXPATHLEN, BU_DIR_CACHE, bu_vls_cstr(&dname), NULL);
// If we're starting over, clear the old working directory
if (!s->resume && bu_file_directory(s->wdir)) {
bu_dirclear(s->wdir);
}
if (!bu_file_directory(s->wdir)) {
// Set up the directory
bu_mkdir(s->wdir);
}
if (!bu_vls_strlen(s->log_file)) {
char tmplfile[MAXPATHLEN];
s->log_file_is_temporary = 1;
bu_vls_sprintf(&dname, "facetize_%s.log", bu_vls_cstr(s->bname));
bu_dir(tmplfile, MAXPATHLEN, s->wdir, bu_vls_cstr(&dname), NULL);
bu_vls_sprintf(s->log_file, "%s", tmplfile);
}
bu_vls_free(&dname);
s->lfile = fopen(bu_vls_cstr(s->log_file), "a");
if (!s->lfile) {
bu_vls_printf(gedp->ged_result_str, "Unable to open log file %s for writing\n", bu_vls_cstr(s->log_file));
ret = BRLCAD_ERROR;
goto ged_facetize_memfree;
}
if (s->execution.processes_breps() &&
NEAR_ZERO(s->nonovlp_threshold, SMALL_FASTF)) {
bu_vls_printf(gedp->ged_result_str,
"-B option requires a specified length threshold\n");
ret = BRLCAD_ERROR;
goto ged_facetize_done;
}
/* If we're doing the experimental brep-only logic, it's a separate process */
if (s->nonovlp_brep) {
if (NEAR_ZERO(s->nonovlp_threshold, SMALL_FASTF)) {
bu_vls_printf(gedp->ged_result_str, "-B option requires a specified length threshold\n");
ret = BRLCAD_ERROR;
goto ged_facetize_memfree;
}
ret = _nonovlp_brep_facetize(s, argc, argv);
goto ged_facetize_memfree;
if (facetize_build_plan(s, argc, argv, plan) != BRLCAD_OK ||
facetize_prepare_workspace(s, plan) != BRLCAD_OK) {
ret = BRLCAD_ERROR;
goto ged_facetize_done;
}
ret = facetize_execute_plan(s, plan);
/* Multi-region mode has a different processing logic */
if (s->regions) {
ret = _ged_facetize_regions(s, argc, argv);
} else {
ret = _ged_facetize_objs(s, argc, argv);
}
ged_facetize_memfree:
facetize_summary(s);
_ged_facetize_state_destroy(s);
delete method_options;
ged_facetize_done:
return ret;
}
+27 -17
View File
@@ -36,11 +36,12 @@
#include "vmath.h"
#include "bu/vls.h"
#include "bn/tol.h"
#include "bg/spsr.h"
#include "rt/geom.h"
#include "raytrace.h"
#include "ged/defines.h"
#include "./orchestrator.h"
__BEGIN_DECLS
#define FACETIZE_METHOD_ATTR "facetize_method"
@@ -50,10 +51,7 @@ struct _ged_facetize_state {
// Output
int verbosity;
int no_empty;
int make_nmg;
int nonovlp_brep;
int no_fixup;
int no_perturb;
int use_variant_plan;
int tolerate_failures;
int tolerated_failures;
@@ -80,14 +78,13 @@ struct _ged_facetize_state {
struct bu_vls *inspection_log;
// Processing
int regions;
FacetizeExecutionOptions execution;
int resume;
int in_place;
int nmg_booleval;
// Settings
int max_time;
int max_pnts;
int max_workers;
struct bu_vls *prefix;
struct bu_vls *suffix;
@@ -102,7 +99,6 @@ struct _ged_facetize_state {
struct db_i *dbip;
union tree *facetize_tree;
void *method_opts;
void *log_s;
/* Cache-local write measurements seed and refine worker write deadlines. */
int write_profiled;
@@ -132,13 +128,15 @@ extern int
_db_uniq_test(struct bu_vls *n, void *data);
extern int
_ged_validate_objs_list(struct _ged_facetize_state *s, int argc, const char *argv[], int newobj_cnt);
_ged_facetize_regions(struct _ged_facetize_state *s, const FacetizePlan &plan);
extern int
_ged_facetize_regions(struct _ged_facetize_state *s, int argc, const char **argv);
_ged_facetize_objs(struct _ged_facetize_state *s, const FacetizePlan &plan);
extern int
_ged_facetize_nmgeval(struct _ged_facetize_state *s, int argc, const char **argv, const char *newname);
_ged_facetize_nmgeval(struct _ged_facetize_state *s,
struct db_i *target_dbip, const char *database_path,
const std::vector<std::string> &input_names, const char *newname);
extern int
_ged_facetize_booleval(struct _ged_facetize_state *s, int argc, struct directory **dpa, const char *newname, bool output_to_working, bool cleanup);
@@ -158,7 +156,16 @@ _ged_facetize_leaves_tri(struct _ged_facetize_state *s, struct db_i *dbip, struc
extern int
_ged_facetize_booleval_tri(struct _ged_facetize_state *s, struct db_i *dbip, struct rt_wdb *wdbp, int argc, const char **argv, const char *newname, struct bu_list *vlfree, bool output_to_working, int curr_cnt, int total_cnt);
extern int _nonovlp_brep_facetize(struct _ged_facetize_state *s, int argc, const char **argv);
/**
* Evaluate a tessellated CSG tree and write the resulting BoT to an explicit
* database. The input database is never modified unless it is also supplied
* as @p output_dbip. This form lets isolated workers evaluate into an
* in-memory database before entering the staged-write protocol.
*/
extern int
_ged_facetize_booleval_tri_to_db(struct _ged_facetize_state *s, struct db_i *dbip, struct rt_wdb *wdbp, int argc, const char **argv, const char *newname, struct bu_list *vlfree, struct db_i *output_dbip, int curr_cnt, int total_cnt);
extern int _nonovlp_brep_facetize(struct _ged_facetize_state *s, const FacetizePlan &plan);
extern struct rt_bot_internal *
bot_fixup(struct _ged_facetize_state *s, struct db_i *wdbip, struct directory *bot_dp, const char *bname);
@@ -229,17 +236,20 @@ struct FacetizeVariantPlan {
* Returns an allocated FacetizeVariantPlan owned by the caller (or NULL on
* allocation failure). Primitives without ft_perturb support are counted in
* n_perturb_fallbacks and will fall back to the original mesh at booleval time.
* If working_dbip is non-NULL it must be an indexed writable handle for the
* working database; it is borrowed and remains open on return.
*/
extern FacetizeVariantPlan *
_ged_facetize_build_variant_plan(struct _ged_facetize_state *s,
int argc,
struct directory **dpa);
struct directory **dpa,
struct db_i *working_dbip);
/**
* Tessellate the variant primitives in the working .g using the NMG method.
* Called after _ged_facetize_leaves_tri() once original leaves are already
* BoTs, either eagerly for direct booleval or lazily on the first
* validation-triggered retry in region mode. Updates
* Tessellate the variant primitives in the working .g using the configured
* primitive methods and options. Called after _ged_facetize_leaves_tri() once
* original leaves are already BoTs, either eagerly for direct booleval or
* lazily on the first validation-triggered retry in region mode. Updates
* plan->n_variant_tess_failures for any variants that could not be
* tessellated (they will silently fall back to the original mesh at booleval).
*/
+158 -380
View File
@@ -19,405 +19,183 @@
*/
/** @file libged/facetize/nmg.cpp
*
* Classic NMG boolean evaluation path.
*
* Parent-side launch policy for isolated NMG Boolean evaluation.
*/
#include "common.h"
#include <string.h>
#include "bio.h"
#include <string>
#include <vector>
#include "bu/app.h"
#include "bu/datetime.h"
#include "bu/file.h"
#include "bu/hook.h"
#include "bu/vls.h"
#include "bu/log.h"
#include "bu/path.h"
#include "bu/process.h"
#include "bu/snooze.h"
#include "raytrace.h"
#include "../ged_private.h"
#include "./ged_facetize.h"
#include "./process.h"
#include "./transfer.h"
#include "./worker.h"
struct _ged_facetize_logging_state {
struct bu_hook_list *saved_bomb_hooks;
struct bu_hook_list *saved_log_hooks;
struct bu_vls *nmg_log;
struct bu_vls *nmg_log_header;
int nmg_log_print_header;
int stderr_stashed;
int serr;
int fnull;
};
extern "C" {
static int
_facetize_bomb_hook(void *cdata, void *str)
{
struct _ged_facetize_state *o = (struct _ged_facetize_state *)cdata;
struct _ged_facetize_logging_state *log_s = (struct _ged_facetize_logging_state *)o->log_s;
if (log_s->nmg_log_print_header) {
bu_vls_printf(log_s->nmg_log, "%s\n", bu_vls_addr(log_s->nmg_log_header));
log_s->nmg_log_print_header = 0;
}
bu_vls_printf(log_s->nmg_log, "%s\n", (const char *)str);
return 0;
}
static int
_facetize_nmg_logging_hook(void *data, void *str)
{
struct _ged_facetize_state *o = (struct _ged_facetize_state *)data;
struct _ged_facetize_logging_state *log_s = (struct _ged_facetize_logging_state *)o->log_s;
if (log_s->nmg_log_print_header) {
bu_vls_printf(log_s->nmg_log, "%s\n", bu_vls_addr(log_s->nmg_log_header));
log_s->nmg_log_print_header = 0;
}
bu_vls_printf(log_s->nmg_log, "%s\n", (const char *)str);
return 0;
}
static void
_facetize_log_nmg(struct _ged_facetize_state *o)
{
if (fileno(stderr) < 0)
return;
/* Seriously, bu_bomb, we don't want you blathering
* to stderr... shut down stderr temporarily. */
struct _ged_facetize_logging_state *log_s = (struct _ged_facetize_logging_state *)o->log_s;
log_s->fnull = open(bu_file_null(), O_WRONLY);
if (log_s->fnull != -1) {
log_s->serr = fileno(stderr);
log_s->stderr_stashed = dup(log_s->serr);
dup2(log_s->fnull, log_s->serr);
close(log_s->fnull);
}
/* Set bu_log logging to capture in nmg_log, rather than the
* application defaults */
bu_log_hook_delete_all();
bu_log_add_hook(_facetize_nmg_logging_hook, (void *)o);
/* Also engage the nmg bomb hooks */
bu_bomb_delete_all_hooks();
bu_bomb_add_hook(_facetize_bomb_hook, (void *)o);
}
static void
_facetize_log_default(struct _ged_facetize_state *o)
{
if (fileno(stderr) < 0 || !o || !o->log_s)
return;
/* Put stderr back */
struct _ged_facetize_logging_state *log_s = (struct _ged_facetize_logging_state *)o->log_s;
if (log_s->fnull != -1) {
fflush(stderr);
dup2(log_s->stderr_stashed, log_s->serr);
close(log_s->stderr_stashed);
log_s->fnull = -1;
}
/* Restore bu_bomb hooks to the application defaults */
bu_bomb_delete_all_hooks();
bu_bomb_restore_hooks(log_s->saved_bomb_hooks);
/* Restore bu_log hooks to the application defaults */
bu_log_hook_delete_all();
bu_log_hook_restore_all(log_s->saved_log_hooks);
}
}
static union tree *
facetize_region_end(struct db_tree_state *tsp,
const struct db_full_path *pathp,
union tree *curtree,
void *client_data)
{
union tree **facetize_tree;
if (tsp) RT_CK_DBTS(tsp);
if (pathp) RT_CK_FULL_PATH(pathp);
struct _ged_facetize_state *s = (struct _ged_facetize_state *)client_data;
facetize_tree = &s->facetize_tree;
if (curtree->tr_op == OP_NOP) return curtree;
if (*facetize_tree) {
union tree *tr;
BU_ALLOC(tr, union tree);
RT_TREE_INIT(tr);
tr->tr_op = OP_UNION;
tr->tr_b.tb_regionp = REGION_NULL;
tr->tr_b.tb_left = *facetize_tree;
tr->tr_b.tb_right = curtree;
*facetize_tree = tr;
} else {
*facetize_tree = curtree;
}
/* Tree has been saved, and will be freed later */
return TREE_NULL;
}
static union tree *
facetize_nmg_leaf_tess(struct db_tree_state *tsp, const struct db_full_path *pathp, struct rt_db_internal *ip, void *client_data)
{
union tree *ret = rt_booltree_leaf_tess(tsp, pathp, ip, NULL);
if (!ret) {
struct _ged_facetize_state *s = (struct _ged_facetize_state *)client_data;
if (s && s->tolerate_failures && pathp) {
char *path_str = db_path_to_string(pathp);
facetize_tolerated_failure(s, "NMG leaf tessellation failed for '%s'; leaf will be omitted from boolean evaluation", path_str ? path_str : "(unknown)");
if (path_str)
bu_free(path_str, "path string");
}
}
return ret;
}
static struct model *
_try_nmg_facetize(struct _ged_facetize_state *s, struct bu_list *vlfree, int argc, const char **argv)
{
struct db_i *dbip = s->dbip;
int i;
int failed = 0;
struct db_tree_state init_state;
union tree *facetize_tree;
struct model *nmg_model;
struct rt_wdb *wdbp = wdb_dbopen(dbip, RT_WDB_TYPE_DB_DEFAULT);
struct _ged_facetize_logging_state *log_s;
BU_GET(log_s, struct _ged_facetize_logging_state);
BU_GET(log_s->nmg_log, struct bu_vls);
bu_vls_init(log_s->nmg_log);
BU_GET(log_s->nmg_log_header, struct bu_vls);
bu_vls_init(log_s->nmg_log_header);
s->log_s = log_s;
_facetize_log_nmg(s);
db_init_db_tree_state(&init_state, dbip);
/* Establish tolerances */
init_state.ts_ttol = &wdbp->wdb_ttol;
init_state.ts_tol = &wdbp->wdb_tol;
s->facetize_tree = (union tree *)0;
facetize_tree = (union tree *)0;
nmg_model = nmg_mm();
init_state.ts_m = &nmg_model;
if (!BU_SETJUMP) {
/* try */
i = db_walk_tree(dbip, argc, (const char **)argv,
1,
&init_state,
0, /* take all regions */
facetize_region_end,
facetize_nmg_leaf_tess,
(void *)s
);
} else {
/* catch */
BU_UNSETJUMP;
_facetize_log_default(s);
bu_vls_free(log_s->nmg_log_header);
bu_vls_free(log_s->nmg_log);
BU_PUT(log_s->nmg_log_header, struct _ged_facetize_logging_state);
BU_PUT(log_s->nmg_log, struct _ged_facetize_logging_state);
BU_PUT(log_s, struct _ged_facetize_logging_state);
s->log_s = NULL;
if (s->facetize_tree) {
db_free_tree(s->facetize_tree);
s->facetize_tree = NULL;
}
return NULL;
} BU_UNSETJUMP;
facetize_tree = s->facetize_tree;
if (i < 0) {
/* Destroy NMG */
_facetize_log_default(s);
bu_vls_free(log_s->nmg_log_header);
bu_vls_free(log_s->nmg_log);
BU_PUT(log_s->nmg_log_header, struct _ged_facetize_logging_state);
BU_PUT(log_s->nmg_log, struct _ged_facetize_logging_state);
BU_PUT(log_s, struct _ged_facetize_logging_state);
s->log_s = NULL;
if (s->facetize_tree) {
db_free_tree(s->facetize_tree);
s->facetize_tree = NULL;
}
return NULL;
}
if (facetize_tree) {
if (!BU_SETJUMP) {
/* try */
failed = nmg_boolean(facetize_tree, nmg_model, vlfree, &wdbp->wdb_tol);
} else {
/* catch */
BU_UNSETJUMP;
_facetize_log_default(s);
bu_vls_free(log_s->nmg_log_header);
bu_vls_free(log_s->nmg_log);
BU_PUT(log_s->nmg_log_header, struct _ged_facetize_logging_state);
BU_PUT(log_s->nmg_log, struct _ged_facetize_logging_state);
BU_PUT(log_s, struct _ged_facetize_logging_state);
s->log_s = NULL;
if (s->facetize_tree) {
db_free_tree(s->facetize_tree);
s->facetize_tree = NULL;
}
return NULL;
} BU_UNSETJUMP;
} else {
failed = 1;
}
if (!failed && facetize_tree) {
NMG_CK_REGION(facetize_tree->tr_d.td_r);
facetize_tree->tr_d.td_r = (struct nmgregion *)NULL;
}
if (failed && s->tolerate_failures) {
facetize_tolerated_failure(s, "NMG boolean evaluation failed after leaf tessellation; no partial NMG result could be generated for this tree");
}
if (facetize_tree) {
db_free_tree(facetize_tree);
s->facetize_tree = NULL;
}
_facetize_log_default(s);
bu_vls_free(log_s->nmg_log_header);
bu_vls_free(log_s->nmg_log);
BU_PUT(log_s->nmg_log_header, struct _ged_facetize_logging_state);
BU_PUT(log_s->nmg_log, struct _ged_facetize_logging_state);
BU_PUT(log_s, struct _ged_facetize_logging_state);
s->log_s = NULL;
return (failed) ? NULL : nmg_model;
}
static int
_write_nmg(struct _ged_facetize_state *s, struct model *nmg_model, const char *name)
{
struct db_i *dbip = s->dbip;
struct rt_db_internal intern;
struct directory *dp;
/* Export NMG as a new solid */
RT_DB_INTERNAL_INIT(&intern);
intern.idb_major_type = DB5_MAJORTYPE_BRLCAD;
intern.idb_type = ID_NMG;
intern.idb_meth = &OBJ[ID_NMG];
intern.idb_ptr = (void *)nmg_model;
dp = db_diradd(dbip, name, RT_DIR_PHONY_ADDR, 0, RT_DIR_SOLID, (void *)&intern.idb_type);
if (dp == RT_DIR_NULL) {
if (s->verbosity > 0) {
bu_log("Cannot add %s to directory\n", name);
}
return BRLCAD_ERROR;
}
if (rt_db_put_internal(dp, dbip, &intern) < 0) {
if (s->verbosity > 0) {
bu_log("Failed to write %s to database\n", name);
}
rt_db_free_internal(&intern);
return BRLCAD_ERROR;
}
return BRLCAD_OK;
}
static int
_write_bot(struct _ged_facetize_state *s, struct rt_bot_internal *bot, const char *name)
{
struct db_i *dbip = s->dbip;
struct rt_db_internal intern;
struct directory *dp;
/* Export BoT as a new solid */
RT_DB_INTERNAL_INIT(&intern);
intern.idb_major_type = DB5_MAJORTYPE_BRLCAD;
intern.idb_type = ID_BOT;
intern.idb_meth = &OBJ[ID_BOT];
intern.idb_ptr = (void *)bot;
dp = db_diradd(dbip, name, RT_DIR_PHONY_ADDR, 0, RT_DIR_SOLID, (void *)&intern.idb_type);
if (dp == RT_DIR_NULL) {
if (s->verbosity > 0) {
bu_log("Cannot add %s to directory\n", name);
}
return BRLCAD_ERROR;
}
if (rt_db_put_internal(dp, dbip, &intern) < 0) {
if (s->verbosity > 0) {
bu_log("Failed to write %s to database\n", name);
}
rt_db_free_internal(&intern);
return BRLCAD_ERROR;
}
return BRLCAD_OK;
}
static const int FACETIZE_NMG_POLL_USEC = 1000;
static const char FACETIZE_NMG_RESULT_FILE[] = "nmg_boolean_result.g";
int
_ged_facetize_nmgeval(struct _ged_facetize_state *s, int argc, const char **argv, const char *oname)
_ged_facetize_nmgeval(struct _ged_facetize_state *s,
struct db_i *target_dbip, const char *database_path,
const std::vector<std::string> &input_names, const char *output_name)
{
int ret = BRLCAD_OK;
struct db_i *dbip = s->dbip;
struct rt_wdb *wdbp;
struct rt_bot_internal *bot = NULL;
struct bu_list *vlfree = &rt_vlfree;
struct model *nmg_model = _try_nmg_facetize(s, vlfree, argc, argv);
if (!s || !target_dbip || !database_path || !database_path[0] ||
input_names.empty() || !output_name || !output_name[0] || !s->wdir)
return BRLCAD_ERROR;
if (nmg_model == NULL) {
if (s->verbosity > 1) {
bu_log("NMG(%s): no resulting region, aborting\n", oname);
FacetizeWorkerRequest request;
request.operation = s->execution.writes_nmg() ?
FacetizeWorkerOperation::NmgBooleanToNmg :
FacetizeWorkerOperation::NmgBooleanToBot;
request.input_names = input_names;
request.output_name = output_name;
if (!request.valid())
return BRLCAD_ERROR;
char result_file[MAXPATHLEN];
bu_dir(result_file, MAXPATHLEN, s->wdir, FACETIZE_NMG_RESULT_FILE, NULL);
(void)bu_file_delete(result_file);
char executable[MAXPATHLEN];
bu_dir(executable, MAXPATHLEN, BU_DIR_BIN, "ged_exec", BU_DIR_EXT, NULL);
std::vector<std::string> command;
command.push_back(executable);
command.push_back("facetize_process");
command.push_back(database_path);
struct bu_process *process = NULL;
FILE *process_input = NULL;
FacetizeWorkerClient channel;
if (facetize_process_start(&process, &process_input, channel, command,
result_file, "--nmg-server") != BRLCAD_OK) {
(void)bu_file_delete(result_file);
facetize_log(s, 0,
"FACETIZE: unable to start the NMG Boolean worker for %s\n",
output_name);
return BRLCAD_ERROR;
}
if (!channel.send_request(request)) {
(void)facetize_process_stop(s, &process, process_input, channel, true);
(void)bu_file_delete(result_file);
facetize_log(s, 0,
"FACETIZE: unable to submit %s to the NMG Boolean worker\n",
output_name);
return BRLCAD_ERROR;
}
facetize_log(s, 1, "FACETIZE: evaluating %s with NMG Boolean...\n",
output_name);
FacetizeWorkerStatus status;
int64_t request_start = bu_gettime();
int64_t write_deadline = 0;
bool write_permitted = false;
bool interrupted = false;
bool evaluation_timed_out = false;
bool write_timed_out = false;
while (!status.result_received) {
facetize_process_drain_stdout(s, process, channel, &status);
facetize_process_drain_stderr(s, process);
int64_t now = bu_gettime();
if (status.write_ready && !write_permitted) {
if (s->max_time > 0 && now - request_start >=
BU_SEC2USEC(s->max_time)) {
interrupted = true;
evaluation_timed_out = true;
break;
}
write_permitted = true;
double write_timeout = facetize_write_timeout_seconds(
status.payload_size, 0.0, 0.0);
write_deadline = now + BU_SEC2USEC(write_timeout);
if (!channel.send_write_proceed()) {
interrupted = true;
break;
}
continue;
}
ret = BRLCAD_ERROR;
goto ged_nmg_obj_memfree;
if (status.result_received) {
if (status.write_started && !status.write_done)
interrupted = true;
break;
}
if (bu_process_poll(process, NULL) != 0) {
/* The final status may reach the pipe just before process exit.
* Drain once more before deciding the worker was interrupted. */
facetize_process_drain_stdout(s, process, channel, &status);
facetize_process_drain_stderr(s, process);
interrupted = !status.result_received ||
(status.write_started && !status.write_done);
break;
}
if (!write_permitted && s->max_time > 0 &&
now - request_start >= BU_SEC2USEC(s->max_time)) {
interrupted = true;
evaluation_timed_out = true;
break;
}
if (write_permitted && now >= write_deadline) {
interrupted = true;
write_timed_out = true;
break;
}
(void)bu_snooze(FACETIZE_NMG_POLL_USEC);
}
if (!s->make_nmg) {
wdbp = wdb_dbopen(dbip, RT_WDB_TYPE_DB_DEFAULT);
if (!BU_SETJUMP) {
/* try */
bot = (struct rt_bot_internal *)nmg_mdl_to_bot(nmg_model, vlfree, &wdbp->wdb_tol);
} else {
/* catch */
BU_UNSETJUMP;
return BRLCAD_ERROR;
} BU_UNSETJUMP;
ret = _write_bot(s, bot, oname);
int process_status = facetize_process_stop(s, &process, process_input,
channel, interrupted);
int ret = BRLCAD_ERROR;
if (!interrupted && process_status == BRLCAD_OK &&
status.result_received && status.write_done &&
status.result == BRLCAD_OK) {
int expected_type = request.operation ==
FacetizeWorkerOperation::NmgBooleanToNmg ? ID_NMG : ID_BOT;
ret = facetize_transfer_staged_object(target_dbip, result_file,
output_name, expected_type);
}
(void)bu_file_delete(result_file);
if (ret == BRLCAD_OK) {
facetize_log(s, 1,
"FACETIZE: NMG Boolean evaluation succeeded for %s\n",
output_name);
return BRLCAD_OK;
}
if (evaluation_timed_out) {
facetize_log(s, 0,
"FACETIZE: NMG Boolean evaluation timed out after %d seconds for %s\n",
s->max_time, output_name);
} else if (write_timed_out) {
facetize_log(s, 0,
"FACETIZE: staging the NMG Boolean result timed out for %s\n",
output_name);
} else if (interrupted) {
facetize_log(s, 0,
"FACETIZE: NMG Boolean worker exited while processing %s\n",
output_name);
} else if (process_status != BRLCAD_OK) {
facetize_log(s, 0,
"FACETIZE: NMG Boolean worker exited abnormally after processing %s\n",
output_name);
} else if (status.result != BRLCAD_OK) {
facetize_log(s, 0,
"FACETIZE: NMG Boolean evaluation failed for %s\n",
output_name);
} else {
/* Write the NMG */
ret = _write_nmg(s, nmg_model, oname);
facetize_log(s, 0,
"FACETIZE: unable to transfer the staged NMG Boolean result for %s\n",
output_name);
}
ged_nmg_obj_memfree:
if (s->verbosity >= 0 && ret != BRLCAD_OK) {
bu_log("NMG: failed to generate %s\n", oname);
}
return ret;
return BRLCAD_ERROR;
}
// Local Variables:
File diff suppressed because it is too large Load Diff
-92
View File
@@ -1,92 +0,0 @@
/* O L D _ F A C E T I Z E . H
* BRL-CAD
*
* Copyright (c) 2008-2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this file; see the file named COPYING for more
* information.
*/
/** @file libged/old_facetize.h
*
* The facetize command, circa v7.38.2
*
*/
#include "common.h"
#include "vmath.h"
#include "bu.h"
#include "bg.h"
__BEGIN_DECLS
/* Ideally all of this could be in facetize.cpp, but the open() calls
* used by the logging routines are problematic in C++ with Visual C++. */
struct _old_ged_facetize_opts {
int quiet;
int verbosity;
int regions;
int resume;
int retry;
int in_place;
fastf_t feature_size;
fastf_t feature_scale;
fastf_t d_feature_size;
int max_time;
struct bg_tess_tol *tol;
struct bu_vls *faceted_suffix;
/* NMG specific options */
int triangulate;
int make_nmg;
int nmgbool;
int manifold;
int screened_poisson;
int continuation;
int method_flags;
int nmg_use_tnurbs;
/* Poisson specific options */
int max_pnts;
struct bg_3d_spsr_opts s_opts;
/* Brep specific options */
double nonovlp_threshold;
/* internal */
struct bu_attribute_value_set *c_map;
struct bu_attribute_value_set *s_map;
struct bu_hook_list *saved_bomb_hooks;
struct bu_hook_list *saved_log_hooks;
struct bu_vls *nmg_log;
struct bu_vls *nmg_log_header;
int nmg_log_print_header;
int stderr_stashed;
int serr;
int fnull;
struct bu_vls *froot;
struct bu_vls *manifold_comb;
struct bu_vls *nmg_comb;
struct bu_vls *continuation_comb;
struct bu_vls *spsr_comb;
};
extern void
_old_ged_facetize_log_nmg(struct _old_ged_facetize_opts *o);
extern void
_old_ged_facetize_log_default(struct _old_ged_facetize_opts *o);
__END_DECLS
-119
View File
@@ -1,119 +0,0 @@
/* F A C E T I Z E _ L O G . C
* BRL-CAD
*
* Copyright (c) 2008-2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this file; see the file named COPYING for more
* information.
*/
/** @file libged/facetize_log.c
*
* logging mechanisms for the facetize command.
*
*/
#include "common.h"
#include <string.h>
#include "bio.h"
#include "bu/hook.h"
#include "bu/vls.h"
#include "../ged_private.h"
#include "./old_facetize.h"
static int
_ged_facetize_bomb_hook(void *cdata, void *str)
{
struct _old_ged_facetize_opts *o = (struct _old_ged_facetize_opts *)cdata;
if (o->nmg_log_print_header) {
bu_vls_printf(o->nmg_log, "%s\n", bu_vls_addr(o->nmg_log_header));
o->nmg_log_print_header = 0;
}
bu_vls_printf(o->nmg_log, "%s\n", (const char *)str);
return 0;
}
static int
_ged_facetize_nmg_logging_hook(void *data, void *str)
{
struct _old_ged_facetize_opts *o = (struct _old_ged_facetize_opts *)data;
if (o->nmg_log_print_header) {
bu_vls_printf(o->nmg_log, "%s\n", bu_vls_addr(o->nmg_log_header));
o->nmg_log_print_header = 0;
}
bu_vls_printf(o->nmg_log, "%s\n", (const char *)str);
return 0;
}
void
_old_ged_facetize_log_nmg(struct _old_ged_facetize_opts *o)
{
if (fileno(stderr) < 0)
return;
/* Seriously, bu_bomb, we don't want you blathering
* to stderr... shut down stderr temporarily. */
o->fnull = open(bu_file_null(), O_WRONLY);
if (o->fnull != -1) {
o->serr = fileno(stderr);
o->stderr_stashed = dup(o->serr);
dup2(o->fnull, o->serr);
close(o->fnull);
}
/* Set bu_log logging to capture in nmg_log, rather than the
* application defaults */
bu_log_hook_delete_all();
bu_log_add_hook(_ged_facetize_nmg_logging_hook, (void *)o);
/* Also engage the nmg bomb hooks */
bu_bomb_delete_all_hooks();
bu_bomb_add_hook(_ged_facetize_bomb_hook, (void *)o);
}
void
_old_ged_facetize_log_default(struct _old_ged_facetize_opts *o)
{
if (fileno(stderr) < 0)
return;
/* Put stderr back */
if (o->fnull != -1) {
fflush(stderr);
dup2(o->stderr_stashed, o->serr);
close(o->stderr_stashed);
o->fnull = -1;
}
/* Restore bu_bomb hooks to the application defaults */
bu_bomb_delete_all_hooks();
bu_bomb_restore_hooks(o->saved_bomb_hooks);
/* Restore bu_log hooks to the application defaults */
bu_log_hook_delete_all();
bu_log_hook_restore_all(o->saved_log_hooks);
}
/*
* Local Variables:
* tab-width: 8
* mode: C
* indent-tabs-mode: t
* c-file-style: "stroustrup"
* End:
* ex: shiftwidth=4 tabstop=8
*/
+359
View File
@@ -0,0 +1,359 @@
/* O R C H E S T R A T O R . C P P
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*/
/** @file libged/facetize/orchestrator.cpp
*
* Command planning, session lifetime, and workspace ownership for facetize.
*/
#include "common.h"
#include <cstring>
#include <cstdio>
#include <fstream>
#include <sstream>
#include <string>
#include <sys/stat.h>
#include "bu/app.h"
#include "bu/file.h"
#include "bu/path.h"
#include "bu/vls.h"
#include "../ged_private.h"
#include "./ged_facetize.h"
#include "./tess_opts.h"
static const unsigned int FACETIZE_WORKSPACE_SCHEMA = 1;
static const char FACETIZE_MANIFEST_NAME[] = "request.manifest";
std::vector<const char *>
FacetizePlan::input_argv() const
{
std::vector<const char *> argv;
argv.reserve(inputs.size());
for (const std::string &input : inputs)
argv.push_back(input.c_str());
return argv;
}
static struct _ged_facetize_state *
facetize_state_create()
{
struct _ged_facetize_state *s = NULL;
BU_GET(s, struct _ged_facetize_state);
s->verbosity = 0;
s->no_empty = 0;
s->no_fixup = 0;
s->use_variant_plan = 1;
s->tolerate_failures = 0;
s->tolerated_failures = 0;
s->tolerated_failure_details = 0;
s->tolerated_failure_omitted = 0;
s->inspection_regions = 0;
s->perturb_sa_tol = 10.0;
s->perturb_vol_tol = 10.0;
s->wdir = NULL;
BU_GET(s->log_file, struct bu_vls);
bu_vls_init(s->log_file);
s->log_file_is_temporary = 0;
s->lfile = NULL;
BU_GET(s->failure_msg, struct bu_vls);
bu_vls_init(s->failure_msg);
BU_GET(s->tolerated_failure_log, struct bu_vls);
bu_vls_init(s->tolerated_failure_log);
BU_GET(s->region_summary, struct bu_vls);
bu_vls_init(s->region_summary);
BU_GET(s->primitive_summary, struct bu_vls);
bu_vls_init(s->primitive_summary);
BU_GET(s->inspection_log, struct bu_vls);
bu_vls_init(s->inspection_log);
BU_GET(s->wfile, struct bu_vls);
bu_vls_init(s->wfile);
BU_GET(s->bname, struct bu_vls);
bu_vls_init(s->bname);
BU_GET(s->suffix, struct bu_vls);
bu_vls_init(s->suffix);
BU_GET(s->prefix, struct bu_vls);
bu_vls_init(s->prefix);
BU_GET(s->solid_suffix, struct bu_vls);
bu_vls_init(s->solid_suffix);
bu_vls_sprintf(s->solid_suffix, ".bot");
s->resume = 0;
s->max_time = 0;
s->max_pnts = 0;
s->max_workers = 0;
s->tol = NULL;
s->nonovlp_threshold = 0;
s->error_flag = 0;
s->gedp = NULL;
s->dbip = NULL;
s->facetize_tree = NULL;
s->method_opts = NULL;
s->write_profiled = 0;
s->write_profile_bytes = 0.0;
s->write_profile_usec = 0.0;
s->variant_plan = NULL;
return s;
}
static void
facetize_state_destroy(struct _ged_facetize_state *s)
{
if (!s)
return;
if (s->wdir)
bu_free(s->wdir, "wdir");
if (s->lfile)
fclose(s->lfile);
struct bu_vls *strings[] = {
s->bname, s->log_file, s->failure_msg, s->tolerated_failure_log,
s->region_summary, s->primitive_summary, s->inspection_log, s->wfile,
s->prefix, s->suffix, s->solid_suffix
};
for (struct bu_vls *str : strings) {
if (!str)
continue;
bu_vls_free(str);
BU_PUT(str, struct bu_vls);
}
delete (FacetizeVariantPlan *)s->variant_plan;
BU_PUT(s, struct _ged_facetize_state);
}
FacetizeSession::FacetizeSession(struct ged *gedp)
{
methods = new method_options_t;
session_state = facetize_state_create();
session_state->gedp = gedp;
session_state->dbip = gedp->dbip;
session_state->method_opts = methods;
}
FacetizeSession::~FacetizeSession()
{
facetize_summary(session_state);
facetize_state_destroy(session_state);
delete methods;
}
int
facetize_build_plan(struct _ged_facetize_state *s, int argc,
const char **argv, FacetizePlan &plan)
{
if (!s || !s->dbip || argc <= 0 || !argv)
return BRLCAD_ERROR;
plan.execution = s->execution;
plan.inputs.clear();
plan.output.clear();
std::vector<std::string> missing;
for (int i = 0; i < argc; i++) {
if (db_lookup(s->dbip, argv[i], LOOKUP_QUIET) == RT_DIR_NULL)
missing.emplace_back(argv[i]);
else
plan.inputs.emplace_back(argv[i]);
}
if (plan.execution.writes_in_place()) {
if (!missing.empty()) {
bu_vls_printf(s->gedp->ged_result_str,
"In-place conversion requires every input object to exist:\n");
for (const std::string &name : missing)
bu_vls_printf(s->gedp->ged_result_str, " %s\n", name.c_str());
return BRLCAD_ERROR;
}
} else {
if (missing.size() != 1) {
if (missing.empty())
bu_vls_printf(s->gedp->ged_result_str,
"A new output object name is required.\n");
else
bu_vls_printf(s->gedp->ged_result_str,
"Exactly one new output object name is required; found %zu.\n",
missing.size());
return BRLCAD_ERROR;
}
plan.output = missing.front();
}
if (plan.inputs.empty()) {
bu_vls_printf(s->gedp->ged_result_str,
"No existing input objects were specified.\n");
return BRLCAD_ERROR;
}
return BRLCAD_OK;
}
static std::string
facetize_request_signature(struct _ged_facetize_state *s,
const FacetizePlan &plan, const char *source_path,
const struct stat &source_stat)
{
std::ostringstream signature;
signature << "schema=" << FACETIZE_WORKSPACE_SCHEMA << '\n';
signature << "source=" << source_path << '\n';
signature << "source_size=" << source_stat.st_size << '\n';
signature << "source_mtime=" << source_stat.st_mtime << '\n';
signature << "scope=" << static_cast<int>(plan.execution.scope) << '\n';
signature << "engine=" << static_cast<int>(plan.execution.boolean_engine) << '\n';
signature << "output_format=" << static_cast<int>(plan.execution.output_format) << '\n';
signature << "commit=" << static_cast<int>(plan.execution.commit_mode) << '\n';
signature << "perturb=" << static_cast<int>(plan.execution.perturb_mode) << '\n';
signature << "no_empty=" << s->no_empty << '\n';
signature << "no_fixup=" << s->no_fixup << '\n';
signature << "tolerate_failures=" << s->tolerate_failures << '\n';
signature << "max_time=" << s->max_time << '\n';
signature << "max_pnts=" << s->max_pnts << '\n';
signature << "max_workers=" << s->max_workers << '\n';
signature << "perturb_sa_tol=" << s->perturb_sa_tol << '\n';
signature << "perturb_vol_tol=" << s->perturb_vol_tol << '\n';
signature << "prefix=" << bu_vls_cstr(s->prefix) << '\n';
signature << "suffix=" << bu_vls_cstr(s->suffix) << '\n';
signature << "output=" << plan.output << '\n';
for (const std::string &input : plan.inputs)
signature << "input=" << input << '\n';
method_options_t *methods = (method_options_t *)s->method_opts;
if (methods) {
for (const std::string &method : methods->methods)
signature << "method=" << method << '\n';
for (const auto &method_entry : methods->options_map)
for (const auto &option_entry : method_entry.second)
signature << "method_option=" << method_entry.first << ':'
<< option_entry.first << '=' << option_entry.second << '\n';
}
return signature.str();
}
static bool
read_file(const char *path, std::string &contents)
{
std::ifstream input(path, std::ios::binary);
if (!input)
return false;
std::ostringstream buffer;
buffer << input.rdbuf();
contents = buffer.str();
return input.good() || input.eof();
}
static bool
write_file(const char *path, const std::string &contents)
{
std::ofstream output(path, std::ios::binary | std::ios::trunc);
if (!output)
return false;
output.write(contents.data(), (std::streamsize)contents.size());
return output.good();
}
int
facetize_prepare_workspace(struct _ged_facetize_state *s,
const FacetizePlan &plan)
{
char source_path[MAXPATHLEN];
if (!bu_file_realpath(s->dbip->dbi_filename, source_path)) {
bu_vls_printf(s->gedp->ged_result_str,
"Unable to resolve database path '%s'.\n", s->dbip->dbi_filename);
return BRLCAD_ERROR;
}
struct stat source_stat;
if (stat(source_path, &source_stat) != 0) {
bu_vls_printf(s->gedp->ged_result_str,
"Unable to inspect database '%s'.\n", source_path);
return BRLCAD_ERROR;
}
bu_path_component(s->bname, source_path, BU_PATH_BASENAME);
unsigned long long path_hash = bu_data_hash(source_path, strlen(source_path));
struct bu_vls workspace_name = BU_VLS_INIT_ZERO;
bu_vls_sprintf(&workspace_name, "facetize_%llu", path_hash);
s->wdir = (char *)bu_calloc(MAXPATHLEN, sizeof(char), "facetize workspace");
bu_dir(s->wdir, MAXPATHLEN, BU_DIR_CACHE, bu_vls_cstr(&workspace_name), NULL);
bool workspace_exists = bu_file_directory(s->wdir);
if (!s->resume && workspace_exists)
bu_dirclear(s->wdir);
if (!bu_file_directory(s->wdir))
bu_mkdir(s->wdir);
if (!bu_file_directory(s->wdir)) {
bu_vls_printf(s->gedp->ged_result_str,
"Unable to create facetize workspace '%s'.\n", s->wdir);
bu_vls_free(&workspace_name);
return BRLCAD_ERROR;
}
char manifest_path[MAXPATHLEN];
bu_dir(manifest_path, MAXPATHLEN, s->wdir, FACETIZE_MANIFEST_NAME, NULL);
std::string expected_manifest = facetize_request_signature(s, plan,
source_path, source_stat);
if (s->resume) {
std::string actual_manifest;
if (!read_file(manifest_path, actual_manifest) || actual_manifest != expected_manifest) {
bu_vls_printf(s->gedp->ged_result_str,
"Cannot resume: workspace does not match the database and facetize options.\n");
bu_vls_free(&workspace_name);
return BRLCAD_ERROR;
}
} else if (!write_file(manifest_path, expected_manifest)) {
bu_vls_printf(s->gedp->ged_result_str,
"Unable to write facetize workspace manifest '%s'.\n", manifest_path);
bu_vls_free(&workspace_name);
return BRLCAD_ERROR;
}
if (!bu_vls_strlen(s->log_file)) {
char log_path[MAXPATHLEN];
s->log_file_is_temporary = 1;
bu_vls_sprintf(&workspace_name, "facetize_%s.log", bu_vls_cstr(s->bname));
bu_dir(log_path, MAXPATHLEN, s->wdir, bu_vls_cstr(&workspace_name), NULL);
bu_vls_sprintf(s->log_file, "%s", log_path);
}
bu_vls_free(&workspace_name);
s->lfile = fopen(bu_vls_cstr(s->log_file), "a");
if (!s->lfile) {
bu_vls_printf(s->gedp->ged_result_str,
"Unable to open log file %s for writing\n", bu_vls_cstr(s->log_file));
return BRLCAD_ERROR;
}
return BRLCAD_OK;
}
int
facetize_execute_plan(struct _ged_facetize_state *s,
const FacetizePlan &plan)
{
if (plan.execution.processes_breps())
return _nonovlp_brep_facetize(s, plan);
if (plan.execution.processes_regions())
return _ged_facetize_regions(s, plan);
return _ged_facetize_objs(s, plan);
}
// Local Variables:
// tab-width: 8
// mode: C++
// c-basic-offset: 4
// indent-tabs-mode: t
// c-file-style: "stroustrup"
// End:
// ex: shiftwidth=4 tabstop=8
+96
View File
@@ -0,0 +1,96 @@
/* O R C H E S T R A T O R . H
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*/
#ifndef LIBGED_FACETIZE_ORCHESTRATOR_H
#define LIBGED_FACETIZE_ORCHESTRATOR_H
#include <string>
#include <vector>
#include "bu/defines.h"
struct ged;
struct _ged_facetize_state;
class method_options_t;
enum class FacetizeOutputFormat {
Bot,
Nmg
};
enum class FacetizeBooleanEngine {
Manifold,
Nmg
};
enum class FacetizeScope {
Objects,
Regions,
NonOverlappingBrep
};
enum class FacetizeCommitMode {
NewObject,
InPlace
};
enum class FacetizePerturbMode {
Disabled,
Enabled
};
struct FacetizeExecutionOptions {
FacetizeOutputFormat output_format = FacetizeOutputFormat::Bot;
FacetizeBooleanEngine boolean_engine = FacetizeBooleanEngine::Manifold;
FacetizeScope scope = FacetizeScope::Objects;
FacetizeCommitMode commit_mode = FacetizeCommitMode::NewObject;
FacetizePerturbMode perturb_mode = FacetizePerturbMode::Disabled;
bool writes_nmg() const { return output_format == FacetizeOutputFormat::Nmg; }
bool uses_nmg_boolean() const { return boolean_engine == FacetizeBooleanEngine::Nmg; }
bool processes_regions() const { return scope == FacetizeScope::Regions; }
bool processes_breps() const { return scope == FacetizeScope::NonOverlappingBrep; }
bool writes_in_place() const { return commit_mode == FacetizeCommitMode::InPlace; }
bool uses_perturbation() const { return perturb_mode == FacetizePerturbMode::Enabled; }
};
struct FacetizePlan {
FacetizeExecutionOptions execution;
std::vector<std::string> inputs;
std::string output;
std::vector<const char *> input_argv() const;
};
class FacetizeSession {
public:
explicit FacetizeSession(struct ged *gedp);
~FacetizeSession();
FacetizeSession(const FacetizeSession &) = delete;
FacetizeSession &operator=(const FacetizeSession &) = delete;
struct _ged_facetize_state *state() const { return session_state; }
method_options_t *method_options() const { return methods; }
private:
struct _ged_facetize_state *session_state = nullptr;
method_options_t *methods = nullptr;
};
int facetize_build_plan(struct _ged_facetize_state *s, int argc,
const char **argv, FacetizePlan &plan);
int facetize_prepare_workspace(struct _ged_facetize_state *s,
const FacetizePlan &plan);
int facetize_execute_plan(struct _ged_facetize_state *s,
const FacetizePlan &plan);
#endif /* LIBGED_FACETIZE_ORCHESTRATOR_H */
+22 -13
View File
@@ -344,7 +344,10 @@ create_variant_in_working_g(struct db_i *wdbip,
point_t bmin, bmax;
/* If bounds fail, keep -1 and fall back to the non-micro/default floor. */
fastf_t bbox_diag = -1.0;
if (rt_bound_internal(src_dbip, src_dp, bmin, bmax) == 0)
struct bn_tol tol;
BN_TOL_INIT(&tol);
if (src_intern.idb_meth && src_intern.idb_meth->ft_bbox &&
src_intern.idb_meth->ft_bbox(&src_intern, &bmin, &bmax, &tol) == 0)
bbox_diag = DIST_PNT_PNT(bmin, bmax);
fastf_t factor = variant_perturb_factor(src_name, is_sub, idx, bbox_diag);
@@ -395,7 +398,8 @@ create_variant_in_working_g(struct db_i *wdbip,
FacetizeVariantPlan *
_ged_facetize_build_variant_plan(struct _ged_facetize_state *s,
int argc,
struct directory **dpa)
struct directory **dpa,
struct db_i *working_dbip)
{
if (!s || !s->dbip || argc <= 0 || !dpa)
return NULL;
@@ -427,18 +431,22 @@ _ged_facetize_build_variant_plan(struct _ged_facetize_state *s,
* name is not in dbnames, then insert the chosen name into
* dbnames to reserve it for subsequent instances.
* ============================================================ */
struct db_i *wdbip =
db_open(bu_vls_cstr(s->wfile), DB_OPEN_READWRITE);
struct db_i *wdbip = working_dbip;
bool close_wdbip = false;
if (!wdbip) {
delete plan;
return NULL;
wdbip = db_open(bu_vls_cstr(s->wfile), DB_OPEN_READWRITE);
if (!wdbip) {
delete plan;
return NULL;
}
close_wdbip = true;
if (db_dirbuild(wdbip) < 0) {
db_close(wdbip);
delete plan;
return NULL;
}
db_update_nref(wdbip);
}
if (db_dirbuild(wdbip) < 0) {
db_close(wdbip);
delete plan;
return NULL;
}
db_update_nref(wdbip);
/* Pre-collect all names currently in the working .g */
std::set<std::string> dbnames;
@@ -553,7 +561,8 @@ _ged_facetize_build_variant_plan(struct _ged_facetize_state *s,
}
}
db_close(wdbip);
if (close_wdbip)
db_close(wdbip);
return plan;
}
+217
View File
@@ -0,0 +1,217 @@
/* P R O C E S S . C P P
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*/
#include "common.h"
#include <algorithm>
#include <fstream>
#include "bu/datetime.h"
#include "bu/file.h"
#include "bu/process.h"
#include "bu/snooze.h"
#include "./ged_facetize.h"
#include "./process.h"
static const size_t FACETIZE_PROCESS_IO_BUFFER_SIZE = 4096u;
static const size_t FACETIZE_FILE_COPY_BUFFER_SIZE = 1024u * 1024u;
static const int FACETIZE_PROCESS_POLL_USEC = 1000;
static const int FACETIZE_PROCESS_SHUTDOWN_TIMEOUT_SEC = 5;
static const double FACETIZE_USEC_TO_SEC_DIVISOR = 1.0e6;
static const double FACETIZE_WRITE_TIMEOUT_FACTOR = 10.0;
static const double FACETIZE_WRITE_TIMEOUT_MIN_SEC = 5.0;
static const double FACETIZE_WRITE_TIMEOUT_MAX_SEC = 300.0;
const size_t FACETIZE_WRITE_PROFILE_MIN_BYTES = 1024u * 1024u;
void
facetize_process_drain_stdout(struct _ged_facetize_state *state,
struct bu_process *process, FacetizeWorkerClient &channel,
FacetizeWorkerStatus *status)
{
char buffer[FACETIZE_PROCESS_IO_BUFFER_SIZE];
int fd = bu_process_fileno(process, BU_PROCESS_STDOUT);
FacetizeWorkerStatus ignored_status;
FacetizeWorkerStatus *output_status = status ? status : &ignored_status;
std::vector<std::string> diagnostics;
while (fd >= 0 && bu_process_pending(fd)) {
int count = bu_process_read_n(process, BU_PROCESS_STDOUT,
(int)sizeof(buffer), buffer);
if (count <= 0)
break;
channel.consume_output(buffer, (size_t)count, *output_status,
diagnostics);
}
for (const std::string &diagnostic : diagnostics)
facetize_log(state, 1, "%s\n", diagnostic.c_str());
}
void
facetize_process_drain_stderr(struct _ged_facetize_state *state,
struct bu_process *process)
{
char buffer[FACETIZE_PROCESS_IO_BUFFER_SIZE];
int fd = bu_process_fileno(process, BU_PROCESS_STDERR);
while (fd >= 0 && bu_process_pending(fd)) {
int count = bu_process_read_n(process, BU_PROCESS_STDERR,
(int)sizeof(buffer), buffer);
if (count <= 0)
break;
facetize_log(state, 1, "%.*s", count, buffer);
}
}
int
facetize_process_reap(struct _ged_facetize_state *state,
struct bu_process **process, FacetizeWorkerClient &channel,
bool terminate)
{
if (!process || !*process)
return BRLCAD_ERROR;
if (terminate)
(void)bu_process_terminate(*process);
int64_t deadline = bu_gettime() +
BU_SEC2USEC(FACETIZE_PROCESS_SHUTDOWN_TIMEOUT_SEC);
int poll_result = bu_process_poll(*process, NULL);
while (poll_result == 0 && bu_gettime() < deadline) {
facetize_process_drain_stdout(state, *process, channel, NULL);
facetize_process_drain_stderr(state, *process);
(void)bu_snooze(FACETIZE_PROCESS_POLL_USEC);
poll_result = bu_process_poll(*process, NULL);
}
facetize_process_drain_stdout(state, *process, channel, NULL);
facetize_process_drain_stderr(state, *process);
if (poll_result != 1)
(void)bu_process_terminate(*process);
return bu_process_wait_n(process, 0);
}
int
facetize_process_stop(struct _ged_facetize_state *state,
struct bu_process **process, FILE *&process_input,
FacetizeWorkerClient &channel, bool terminate)
{
int status = facetize_process_reap(state, process, channel, terminate);
process_input = NULL;
channel.reset(NULL);
return status;
}
int
facetize_process_start(struct bu_process **process, FILE **process_input,
FacetizeWorkerClient &channel,
const std::vector<std::string> &command,
const std::string &result_file, const char *mode)
{
if (!process || !process_input || !mode)
return BRLCAD_ERROR;
std::vector<const char *> process_command;
for (const std::string &argument : command)
process_command.push_back(argument.c_str());
if (!result_file.empty()) {
process_command.push_back("--result-file");
process_command.push_back(result_file.c_str());
}
process_command.push_back(mode);
process_command.push_back(NULL);
*process = NULL;
*process_input = NULL;
channel.reset(NULL);
bu_process_create(process, process_command.data(), BU_PROCESS_HIDE_WINDOW);
if (*process)
*process_input = bu_process_file_open(*process, BU_PROCESS_STDIN);
if (*process && *process_input &&
setvbuf(*process_input, NULL, _IONBF, 0) == 0) {
channel.reset(*process_input);
return BRLCAD_OK;
}
if (*process) {
(void)bu_process_terminate(*process);
(void)bu_process_wait_n(process, 0);
}
*process_input = NULL;
return BRLCAD_ERROR;
}
double
facetize_write_timeout_seconds(size_t payload_size,
double profiled_write_bytes, double profiled_write_usec)
{
if (profiled_write_bytes <= 0.0 || profiled_write_usec <= 0.0)
return FACETIZE_WRITE_TIMEOUT_MAX_SEC;
double bytes_per_second = profiled_write_bytes *
FACETIZE_USEC_TO_SEC_DIVISOR / profiled_write_usec;
double projected_seconds = (double)payload_size / bytes_per_second;
double timeout_seconds = projected_seconds * FACETIZE_WRITE_TIMEOUT_FACTOR;
return std::min(FACETIZE_WRITE_TIMEOUT_MAX_SEC,
std::max(FACETIZE_WRITE_TIMEOUT_MIN_SEC, timeout_seconds));
}
int
facetize_file_copy(const char *source, const char *destination,
FacetizeFileCopyProgress progress, void *progress_data)
{
if (!source || !destination)
return BRLCAD_ERROR;
std::ifstream input(source, std::ios::binary);
if (!input.is_open())
return BRLCAD_ERROR;
std::ofstream output(destination, std::ios::binary | std::ios::trunc);
if (!output.is_open())
return BRLCAD_ERROR;
std::vector<char> buffer(FACETIZE_FILE_COPY_BUFFER_SIZE);
uint64_t bytes_copied = 0;
bool copy_succeeded = true;
while (input) {
input.read(buffer.data(), (std::streamsize)buffer.size());
std::streamsize count = input.gcount();
if (count <= 0)
break;
output.write(buffer.data(), count);
if (!output.good()) {
copy_succeeded = false;
break;
}
bytes_copied += (uint64_t)count;
if (progress)
progress(bytes_copied, progress_data);
}
copy_succeeded = copy_succeeded && input.eof() && !input.bad();
if (copy_succeeded) {
output.flush();
copy_succeeded = output.good();
}
input.close();
output.close();
if (!copy_succeeded)
bu_file_delete(destination);
return copy_succeeded ? BRLCAD_OK : BRLCAD_ERROR;
}
// Local Variables:
// tab-width: 8
// mode: C++
// c-basic-offset: 4
// indent-tabs-mode: t
// c-file-style: "stroustrup"
// End:
// ex: shiftwidth=4 tabstop=8
+64
View File
@@ -0,0 +1,64 @@
/* P R O C E S S . H
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*/
#ifndef LIBGED_FACETIZE_PROCESS_H
#define LIBGED_FACETIZE_PROCESS_H
#include <cstdio>
#include <cstdint>
#include <string>
#include <vector>
#include "bu/defines.h"
#include "./worker.h"
struct bu_process;
struct _ged_facetize_state;
extern const size_t FACETIZE_WRITE_PROFILE_MIN_BYTES;
typedef void (*FacetizeFileCopyProgress)(uint64_t bytes_copied, void *data);
void
facetize_process_drain_stdout(struct _ged_facetize_state *state,
struct bu_process *process, FacetizeWorkerClient &channel,
FacetizeWorkerStatus *status);
void
facetize_process_drain_stderr(struct _ged_facetize_state *state,
struct bu_process *process);
int
facetize_process_start(struct bu_process **process, FILE **process_input,
FacetizeWorkerClient &channel,
const std::vector<std::string> &command,
const std::string &result_file, const char *mode);
int
facetize_process_reap(struct _ged_facetize_state *state,
struct bu_process **process, FacetizeWorkerClient &channel,
bool terminate);
int
facetize_process_stop(struct _ged_facetize_state *state,
struct bu_process **process, FILE *&process_input,
FacetizeWorkerClient &channel, bool terminate);
double
facetize_write_timeout_seconds(size_t payload_size,
double profiled_write_bytes, double profiled_write_usec);
int
facetize_file_copy(const char *source, const char *destination,
FacetizeFileCopyProgress progress = NULL, void *progress_data = NULL);
#endif /* LIBGED_FACETIZE_PROCESS_H */
File diff suppressed because it is too large Load Diff
@@ -4,12 +4,18 @@ set(TESS_LIBS libged manifold::manifold)
set(
GED_TESS_SRCS
brep_csg.cpp
continuation.cpp
sample.cpp
spsr.cpp
main.cpp
mdc.cpp
nmg.cpp
nmg_boolean.cpp
util.cpp
../plan.cpp
../process.cpp
../tri.cpp
../transfer.cpp
../util.cpp
../validation.cpp
../worker.cpp
)
@@ -20,6 +26,7 @@ ged_subprocess_library(facetize_process
cmakefiles(
CMakeLists.txt
nmg_boolean.h
tessellate.h
${GED_TESS_SRCS}
)
@@ -1,293 +0,0 @@
/* C O N T I N U A T I O N . C P P
* BRL-CAD
*
* Copyright (c) 2008-2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this file; see the file named COPYING for more
* information.
*/
/** @file libged/facetize/continuation.cpp
*
* The facetize command's Continuation Method implementation.
*/
#include "common.h"
#include <sstream>
#include <string>
#include <vector>
#include "vmath.h"
#include "bu/datetime.h"
#include "bg/trimesh.h"
#include "raytrace.h"
#include "analyze/polygonize.h"
#include "../../ged_private.h"
#include "./tessellate.h"
#define FACETIZE_MEMORY_THRESHOLD 150000000
static void
_pnts_bbox(point_t rpp_min, point_t rpp_max, int pnt_cnt, point_t *pnts)
{
int i = 0;
if (!rpp_min || !rpp_max || !pnts || !pnts || !pnt_cnt) return;
VSETALL(rpp_min, INFINITY);
VSETALL(rpp_max, -INFINITY);
for (i = 0; i < pnt_cnt; i++) {
VMINMAX(rpp_min, rpp_max, pnts[i]);
}
}
static double
_bbox_vol(point_t b_min, point_t b_max)
{
double bbox_vol = 0.0;
fastf_t b_xlen, b_ylen, b_zlen;
b_xlen = fabs(b_max[X] - b_min[X]);
b_ylen = fabs(b_max[Y] - b_min[Y]);
b_zlen = fabs(b_max[Z] - b_min[Z]);
bbox_vol = b_xlen * b_ylen * b_zlen;
return bbox_vol;
}
void free_bot_internal(struct rt_bot_internal *bot)
{
if (!bot)
return;
if (bot->faces)
bu_free(bot->faces, "faces");
if (bot->vertices)
bu_free(bot->vertices, "vertices");
BU_PUT(bot, struct rt_bot_internal);
}
int
bot_gen(struct rt_bot_internal **obot, fastf_t feature_size, point_t seed, const char *objname, struct db_i *dbip, struct analyze_polygonize_params *params)
{
/* Build the BoT */
struct rt_bot_internal *bot;
BU_GET(bot, struct rt_bot_internal);
bot->magic = RT_BOT_INTERNAL_MAGIC;
bot->mode = RT_BOT_SOLID;
bot->orientation = RT_BOT_UNORIENTED;
bot->thickness = (fastf_t *)NULL;
bot->face_mode = (struct bu_bitv *)NULL;
bot->faces = NULL;
bot->vertices = NULL;
int ret = analyze_polygonize(&(bot->faces), (int *)&(bot->num_faces),
(point_t **)&(bot->vertices),
(int *)&(bot->num_vertices),
feature_size, seed, objname, dbip, params);
if (ret == 3)
bu_log("CM: Too little available memory to continue, aborting\n");
if (ret == 2)
bu_log("CM: timed out after %d seconds with size %g\n", params->max_cycle_time, feature_size);
if (!ret) {
*obot = bot;
} else {
free_bot_internal(bot);
}
return ret;
}
int
continuation_mesh(struct rt_bot_internal **obot, struct db_i *dbip, const char *objname, tess_opts *s, point_t seed)
{
int first_run = 1;
int fatal_error_cnt = 0;
double successful_feature_size = 0.0;
unsigned long int successful_bot_count = 1000;
int decimation_succeeded = 0;
struct rt_bot_internal *bot = NULL;
struct rt_bot_internal *dbot = NULL;
int pret = 0;
int total_elapsed = 0;
struct analyze_polygonize_params params = ANALYZE_POLYGONIZE_PARAMS_DEFAULT;
double feature_size = (s->cm_options.feature_size > 0) ? s->cm_options.feature_size : 2*s->cm_options.avg_thickness;
/* Run the polygonize routine. Because it is quite simple to
* accidentally specify inputs that will take huge amounts of time
* to run, we will attempt a series of progressively courser
* polygonize runs until we either succeed, or reach a feature
* size that is greater than 2x the average thickness according to
* the raytracer without succeeding. If max_time has been
* explicitly set to 0 by the caller this will run unbounded, but
* the algorithm is n**2 and we're trying the finest level first
* so may run a *very* long time... */
params.max_time = s->cm_options.max_time;
params.max_cycle_time = s->cm_options.max_cycle_time;
params.verbosity = 1;
params.minimum_free_mem = FACETIZE_MEMORY_THRESHOLD;
while (!pret && (feature_size > 0.9*s->cm_options.target_feature_size) && fatal_error_cnt < 8 ) {
struct rt_bot_internal *candidate = NULL;
double timestamp = bu_gettime();
params.time_offset = total_elapsed + 10; // Allow a few seconds extra for decimation
pret = bot_gen(&candidate, feature_size, seed, objname, dbip, &params);
fastf_t delta = (int)((bu_gettime() - timestamp)/1e6);
if (pret || candidate->num_faces < successful_bot_count || delta < 2) {
if (pret == 3)
break;
if (pret == 2) {
/* If we still haven't had a successful run, back the feature size out and try again */
if (first_run) {
pret = 0;
feature_size = feature_size * 5;
fatal_error_cnt++;
continue;
}
}
if (pret != 2 && !(s->cm_options.feature_size > 0)) {
/* Something about the previous size didn't work - nudge the feature size and try again
* unless we've had multiple fatal errors. */
pret = 0;
bu_log("CM: error at size %g\n", feature_size);
/* If we've had a successful first run, just nudge the feature
* size down and retry. If we haven't succeeded yet, and we've
* got just this one error, try dropping the feature size down
* by an order of magnitude. If we haven't succeed yet *and*
* we've got multiple fatal errors, try dropping it by half. */
feature_size = feature_size * ((!first_run) ? 0.95 : ((fatal_error_cnt) ? 0.5 : 0.1));
bu_log("CM: retrying with size %g\n", feature_size);
fatal_error_cnt++;
continue;
}
feature_size = successful_feature_size;
if (bot && bot->faces) {
if (s->cm_options.feature_size <= 0) {
bu_log("CM: unable to polygonize at target size (%g), using last successful BoT with %d faces, feature size %g\n", s->cm_options.target_feature_size, (int)bot->num_faces, successful_feature_size);
} else {
bu_log("CM: successfully created %d faces, feature size %g\n", (int)bot->num_faces, successful_feature_size);
}
}
} else {
/* if we have had a fatal error in the past, reset on subsequent success */
fatal_error_cnt = 0;
successful_feature_size = feature_size;
bu_log("CM: completed in %g seconds with size %g\n", delta, feature_size);
feature_size = feature_size * ((delta < 5) ? 0.7 : 0.9);
successful_bot_count = candidate->num_faces;
if (bot)
free_bot_internal(bot);
bot = candidate;
}
// If we got this far, we've had a successful first run
first_run = 0;
// If we're about to run up against our max time limit, bail rather than keep trying
total_elapsed += delta;
if (s->cm_options.max_cycle_time < delta*2) {
if (total_elapsed + s->cm_options.max_cycle_time > s->cm_options.max_time) {
feature_size = successful_feature_size;
break;
}
} else {
if (total_elapsed + delta*2 > s->cm_options.max_time) {
feature_size = successful_feature_size;
break;
}
}
}
if (bot && bot->num_faces) {
bu_log("CM: successfully polygonized BoT with %d faces at feature size %g\n", (int)bot->num_faces, successful_feature_size);
}
if (!bot || !bot->faces) {
bu_log("CM: surface reconstruction failed: %s\n", objname);
if (bot && bot->vertices) bu_free(bot->vertices, "verts");
if (bot && bot->faces) bu_free(bot->faces, "verts");
if (bot)
BU_PUT(bot, struct rt_bot_internal *);
return BRLCAD_ERROR;
}
/* do decimation */
{
double d_feature_size = (s->cm_options.d_feature_size > 0) ? s->cm_options.d_feature_size : 1.5 * feature_size;
*obot = bot;
bu_log("CM: decimating with feature size %g\n", d_feature_size);
dbot = _tess_facetize_decimate(bot, d_feature_size);
if (bot == dbot || !dbot->num_vertices || !dbot->num_faces) {
bu_log("decimation failed\n");
return BRLCAD_ERROR;
}
bot = dbot;
decimation_succeeded = 1;
}
/* Get the volume of the bounding box of the decimated BoT */
point_t b_min, b_max;
VSETALL(b_min, INFINITY);
VSETALL(b_max, -INFINITY);
_pnts_bbox(b_min, b_max, bot->num_vertices, (point_t *)bot->vertices);
double bot_bbox_vol = _bbox_vol(b_min, b_max);
/* Check the BoT bbox vol against the bounding box
* of the point cloud - a large difference means something probably isn't
* right. For the moment, use >50% difference. */
if (fabs(s->cm_options.pnts_bbox_vol - bot_bbox_vol) > s->cm_options.pnts_bbox_vol * 0.5) {
if (bot->vertices) bu_free(bot->vertices, "verts");
if (bot->faces) bu_free(bot->faces, "verts");
BU_PUT(bot, struct rt_bot_internal);
return BRLCAD_ERROR;
}
/* Check validity - do not return an invalid BoT */
{
int not_solid = bg_trimesh_solid2(bot->num_vertices, bot->num_faces, (fastf_t *)bot->vertices, (int *)bot->faces, NULL);
if (not_solid) {
if (bot->vertices) bu_free(bot->vertices, "verts");
if (bot->faces) bu_free(bot->faces, "verts");
BU_PUT(bot, struct rt_bot_internal);
bu_log("CM: facetization failed, final BoT was not solid\n");
return BRLCAD_ERROR;
}
}
if (decimation_succeeded) {
bu_log("CM: decimation succeeded, final BoT has %d faces\n", (int)bot->num_faces);
}
*obot = bot;
return BRLCAD_OK;
}
// Local Variables:
// tab-width: 8
// mode: C++
// c-basic-offset: 4
// indent-tabs-mode: t
// c-file-style: "stroustrup"
// End:
// ex: shiftwidth=4 tabstop=8
+535 -102
View File
@@ -34,17 +34,67 @@
#include <string>
#include <vector>
#include <stdlib.h>
#include "bu/app.h"
#include "bu/env.h"
#include "bu/file.h"
#include "bu/opt.h"
#include "bg/trimesh.h"
#include "rt/primitives/bot.h"
#include "ged.h"
#define TESS_OPTS_IMPLEMENTATION
#include "../ged_facetize.h"
#include "../tess_opts.h"
#include "../transfer.h"
#include "../validation.h"
#include "../worker.h"
#include "./nmg_boolean.h"
#include "./tessellate.h"
static const char FACETIZE_TEST_FAULT_ENV[] =
"LIBGED_FACETIZE_TEST_FAULT";
static const char FACETIZE_TEST_FAULT_FILE_ENV[] =
"LIBGED_FACETIZE_TEST_FAULT_FILE";
static const char FACETIZE_TEST_FAULT_TESS_READY[] = "tess-before-ready";
static const char FACETIZE_TEST_FAULT_STAGE_DONE[] = "stage-before-done";
static const char FACETIZE_TEST_FAULT_WRITER_PROCEED[] =
"writer-before-proceed";
static const char FACETIZE_TEST_FAULT_WRITER_DONE[] = "writer-before-done";
static const char FACETIZE_TEST_FAULT_NMG_READY[] = "nmg-before-ready";
/**
* Terminate at a named protocol boundary when explicitly armed by a test.
* Requiring both an exact stage/object selector and a writable one-shot claim
* file keeps this inaccessible during normal operation and prevents restarted
* workers from repeatedly taking the same fault.
*/
static void
facetize_test_fault(const char *stage, const char *object_name)
{
const char *fault = getenv(FACETIZE_TEST_FAULT_ENV);
const char *claim_file = getenv(FACETIZE_TEST_FAULT_FILE_ENV);
if (!fault || !fault[0] || !claim_file || !claim_file[0] ||
!stage || !object_name)
return;
std::string expected = std::string(stage) + ":" + object_name;
if (expected != fault || bu_file_exists(claim_file, NULL))
return;
FILE *claim = fopen(claim_file, "wb");
if (!claim)
return;
bool written = fwrite(expected.data(), 1, expected.size(), claim) ==
expected.size();
bool closed = fclose(claim) == 0;
if (!written || !closed)
return;
bu_exit(BRLCAD_ERROR, "FACETIZE: injected test fault at %s for %s\n",
stage, object_name);
}
static void
facetize_payload_add(size_t *payload_size, size_t count, size_t item_size)
{
@@ -96,19 +146,11 @@ facetize_bot_payload_size(const struct rt_bot_internal *bot)
return payload_size;
}
static void
rt_pnts_free(struct rt_pnts_internal *pnts)
static size_t
facetize_resident_size()
{
struct pnt_normal *rpnt = (struct pnt_normal *)pnts->point;
if (rpnt) {
struct pnt_normal *entry;
while (BU_LIST_WHILE(entry, pnt_normal, &(rpnt->l))) {
BU_LIST_DEQUEUE(&(entry->l));
BU_PUT(entry, struct pnt_normal);
}
BU_PUT(rpnt, struct pnt_normal);
}
BU_PUT(pnts, struct rt_pnts_internal);
ssize_t resident_size = bu_mem(BU_MEM_PROCESS_RESIDENT, NULL);
return (resident_size > 0) ? (size_t)resident_size : 0;
}
static void
@@ -118,28 +160,14 @@ method_setup(tess_opts *s)
return;
std::vector<std::string> *methods = &s->method_opts.methods;
if (!methods->size()) {
methods->push_back(std::string("NMG"));
methods->push_back(std::string("CM"));
methods->push_back(std::string("SPSR"));
}
if (!methods->size())
*methods = tess_default_methods();
// Now that we've set any default overrides for multiple types, get the
// method specific options for each method set up
s->nmg_options.sync(s->method_opts);
s->cm_options.sync(s->method_opts);
s->mdc_options.sync(s->method_opts);
s->spsr_options.sync(s->method_opts);
// Set the sampling options. If CM is active we will be using its settings
// to sample first, so default to those values.
bool sample_sync = false;
if (std::find(methods->begin(), methods->end(), std::string("CM")) != methods->end()) {
s->pnt_options.sync(s->cm_options);
sample_sync = true;
}
if (!sample_sync && std::find(methods->begin(), methods->end(), std::string("SPSR")) != methods->end()) {
s->pnt_options.sync(s->spsr_options);
}
}
static int
@@ -163,8 +191,8 @@ dp_tessellate(struct rt_bot_internal **obot, struct bu_vls *method_flag, struct
}
struct rt_pnts_internal *pnts = NULL;
bool free_pnts = false;
struct rt_bot_internal *bot = NULL;
bool best_effort_source = false;
int propVal;
int ret = BRLCAD_OK;
@@ -199,7 +227,7 @@ dp_tessellate(struct rt_bot_internal **obot, struct bu_vls *method_flag, struct
// If we are going to try a pnts wrapping, there are only a few
// candidates in the fallback methods list that we can use.
mset.erase(std::string("NMG"));
mset.erase(std::string("CM"));
mset.erase(std::string("MDC"));
// point the pnts arguments to the internal point data
pnts = (struct rt_pnts_internal *)intern.idb_ptr;
@@ -227,6 +255,7 @@ dp_tessellate(struct rt_bot_internal **obot, struct bu_vls *method_flag, struct
// Volumetric bot - if it can be manifold we're good, but if
// not we need to try and repair it.
if (!bot_is_manifold(bot)) {
best_effort_source = true;
// Nope - try repairing
struct rt_bot_repair_info settings = RT_BOT_REPAIR_INFO_INIT;
// We're aggressive preparing facetize inputs, since non-lint-passing
@@ -273,53 +302,22 @@ dp_tessellate(struct rt_bot_internal **obot, struct bu_vls *method_flag, struct
}
}
if (mset.find(std::string("CM")) != mset.end()) {
// The continuation method (CM) is a marching algorithm using an
// inside/outside test, building from a seed point on the surface.
//
// CM needs some awareness of properties of the solid, so we use the
// raytrace interrogation to build up that data. Unlike the sampling
// methods we don't make direct use of the points beyond using one of
// them for the seed, but we do use information collected during the
// sampling process.
if (!pnts) {
pnts = _tess_pnts_sample(dp->d_namep, dbip, s);
free_pnts = (pnts != NULL);
}
if (pnts) {
s->cm_options.sync(s->pnt_options);
struct pnt_normal *seed = BU_LIST_PNEXT(pnt_normal, (struct pnt_normal *)pnts->point);
ret = continuation_mesh(obot, dbip, dp->d_namep, s, seed->v);
if (ret == BRLCAD_OK) {
bu_vls_sprintf(method_flag, "CM");
goto dp_tessellate_cleanup;
}
if (mset.find(std::string("MDC")) != mset.end()) {
ret = mdc_mesh(obot, dbip, dp->d_namep, s);
if (ret == BRLCAD_OK) {
bu_vls_sprintf(method_flag, "MDC");
goto dp_tessellate_cleanup;
}
}
pnt_sampling_methods:
if (mset.find(std::string("SPSR")) != mset.end()) {
if (!pnts) {
pnts = _tess_pnts_sample(dp->d_namep, dbip, s);
free_pnts = (pnts != NULL);
} else {
if (!s->spsr_options.equals(s->pnt_options)) {
s->pnt_options.sync(s->spsr_options);
if (free_pnts)
rt_pnts_free(pnts);
pnts = _tess_pnts_sample(dp->d_namep, dbip, s);
free_pnts = (pnts != NULL);
}
}
if (pnts) {
s->spsr_options.sync(s->pnt_options);
ret = spsr_mesh(obot, dbip, pnts, s);
if (ret == BRLCAD_OK) {
bu_vls_sprintf(method_flag, "SPSR");
goto dp_tessellate_cleanup;
}
ret = spsr_mesh(obot, dbip, dp->d_namep, pnts,
best_effort_source, s);
if (ret == BRLCAD_OK) {
bu_vls_sprintf(method_flag, "SPSR");
goto dp_tessellate_cleanup;
}
}
@@ -341,8 +339,6 @@ pnt_sampling_methods:
ret = BRLCAD_ERROR;
dp_tessellate_cleanup:
if (free_pnts && pnts)
rt_pnts_free(pnts);
if (ret != BRLCAD_OK && *obot) {
_tess_facetize_free_bot(*obot);
*obot = NULL;
@@ -355,13 +351,13 @@ void
print_methods_info()
{
nmg_opts nopts;
cm_opts cmopts;
mdc_opts mdcopts;
spsr_opts spsropts;
std::string info;
info.append(nopts.print_options_help());
info.append(std::string("\n"));
info.append(cmopts.print_options_help());
info.append(mdcopts.print_options_help());
info.append(std::string("\n"));
info.append(spsropts.print_options_help());
fprintf(stdout, "%s\n", info.c_str());
@@ -370,36 +366,70 @@ print_methods_info()
void
print_tess_methods()
{
fprintf(stdout, "NMG CM SPSR");
fprintf(stdout, "NMG MDC SPSR\n");
}
static int
facetize_server_request(struct ged *gedp, const char *object_name,
tess_opts *s, FacetizeWorkerServer &worker_channel, bool *result_sent)
facetize_server_request(struct ged *gedp, struct db_i *result_dbip,
const FacetizeWorkerRequest &request,
FacetizeWorkerServer &worker_channel, bool *result_sent)
{
if (!gedp || !object_name || !s || !result_sent)
if (!gedp || !result_sent ||
request.operation != FacetizeWorkerOperation::TessellatePrimitive ||
request.input_names.size() != 1)
return BRLCAD_ERROR;
*result_sent = false;
if (!request.primitive.cache_directory.empty()) {
bu_mkdir(request.primitive.cache_directory.c_str());
if (bu_setenv("BU_DIR_CACHE",
request.primitive.cache_directory.c_str(), 1) != 0)
return BRLCAD_ERROR;
}
tess_opts options;
options.method_opts.methods = request.primitive.methods;
for (const std::string &option_string :
request.primitive.method_options) {
const char *option = option_string.c_str();
if (_tess_method_opts(NULL, 1, &option, &options.method_opts) != 1)
return BRLCAD_ERROR;
}
method_setup(&options);
if (request.primitive.point_limit > 0) {
options.spsr_options.max_pnts = request.primitive.point_limit;
}
const char *object_name = request.input_names.front().c_str();
struct directory *dp = db_lookup(gedp->dbip, object_name, LOOKUP_QUIET);
if (!dp || dp->d_major_type != DB5_MAJORTYPE_BRLCAD)
return BRLCAD_ERROR;
struct rt_bot_internal *obot = NULL;
struct bu_vls method = BU_VLS_INIT_ZERO;
int ret = dp_tessellate(&obot, &method, gedp, dp, s);
int ret = dp_tessellate(&obot, &method, gedp, dp, &options);
if (ret == BRLCAD_OK && obot) {
// Waiting here lets the parent stop an over-time tessellation without
// risking a partially replaced object in the working database.
facetize_test_fault(FACETIZE_TEST_FAULT_TESS_READY, object_name);
size_t payload_size = facetize_bot_payload_size(obot);
if (!worker_channel.send_write_ready(payload_size) ||
!worker_channel.receive_write_proceed()) {
if (!worker_channel.send_write_ready(payload_size,
facetize_resident_size()) ||
!worker_channel.receive_write_proceed() ||
!worker_channel.send_write_started()) {
_tess_facetize_free_bot(obot);
ret = BRLCAD_ERROR;
} else {
ret = _tess_facetize_write_bot(gedp->dbip, obot, object_name,
struct db_i *output_dbip = result_dbip ? result_dbip : gedp->dbip;
const char *output_name = result_dbip ?
FACETIZE_WORKER_RESULT_OBJECT : object_name;
ret = _tess_facetize_write_bot(output_dbip, obot, output_name,
bu_vls_cstr(&method));
if (!worker_channel.send_write_result(ret)) {
if (result_dbip && ret == BRLCAD_OK)
facetize_test_fault(FACETIZE_TEST_FAULT_STAGE_DONE,
object_name);
if (!worker_channel.send_write_result(ret,
facetize_resident_size())) {
ret = BRLCAD_ERROR;
} else {
*result_sent = true;
@@ -413,7 +443,7 @@ facetize_server_request(struct ged *gedp, const char *object_name,
}
static int
facetize_server(const char *work_file, tess_opts *s)
facetize_server(const char *work_file, const char *result_file)
{
int server_status = BRLCAD_OK;
setvbuf(stdout, NULL, _IONBF, 0);
@@ -422,11 +452,18 @@ facetize_server(const char *work_file, tess_opts *s)
if (!gedp)
return BRLCAD_ERROR;
struct db_i *result_dbip = result_file ?
db_create(result_file, BRLCAD_DB_FORMAT_LATEST) : NULL;
if (result_file && !result_dbip) {
ged_close(gedp);
return BRLCAD_ERROR;
}
FacetizeWorkerServer worker_channel(stdin, stdout);
while (true) {
std::string object_name;
FacetizeWorkerRequest request;
FacetizeWorkerReadResult read_result =
worker_channel.receive_request(object_name);
worker_channel.receive_request(request);
if (read_result == FacetizeWorkerReadResult::End)
break;
if (read_result == FacetizeWorkerReadResult::Error) {
@@ -435,14 +472,335 @@ facetize_server(const char *work_file, tess_opts *s)
}
bool result_sent = false;
int ret = facetize_server_request(gedp, object_name.c_str(), s,
worker_channel, &result_sent);
if (!result_sent && !worker_channel.send_tessellation_result(ret)) {
int ret = facetize_server_request(gedp, result_dbip,
request, worker_channel, &result_sent);
if (!result_sent && !worker_channel.send_tessellation_result(ret,
facetize_resident_size())) {
server_status = BRLCAD_ERROR;
break;
}
}
if (result_dbip)
db_close(result_dbip);
ged_close(gedp);
return server_status;
}
static int
facetize_validation_server(const char *source_file)
{
if (!source_file)
return BRLCAD_ERROR;
setvbuf(stdout, NULL, _IONBF, 0);
struct db_i *dbip = db_open(source_file, DB_OPEN_READONLY);
if (!dbip)
return BRLCAD_ERROR;
if (db_dirbuild(dbip) < 0) {
db_close(dbip);
return BRLCAD_ERROR;
}
int server_status = BRLCAD_OK;
FacetizeWorkerServer channel(stdin, stdout);
while (true) {
FacetizeWorkerRequest request;
FacetizeWorkerReadResult read_result =
channel.receive_request(request);
if (read_result == FacetizeWorkerReadResult::End)
break;
if (read_result == FacetizeWorkerReadResult::Error ||
request.operation != FacetizeWorkerOperation::ValidateCsg ||
request.input_names.size() != 1) {
server_status = BRLCAD_ERROR;
break;
}
const std::string &object_name = request.input_names.front();
double surface_area = -1.0;
double volume = -1.0;
long crossings = facetize_csg_metrics(dbip, object_name.c_str(),
&surface_area, &volume);
int result = (crossings >= 0) ? BRLCAD_OK : BRLCAD_ERROR;
if (!channel.send_csg_result(result, crossings, surface_area, volume,
facetize_resident_size())) {
server_status = BRLCAD_ERROR;
break;
}
}
db_close(dbip);
return server_status;
}
static int
facetize_region_server_request(struct _ged_facetize_state *state,
struct db_i *work_dbip, struct rt_wdb *work_wdbp,
struct db_i *staging_dbip, struct rt_wdb *staging_wdbp,
const char *object_name, FacetizeWorkerServer &channel,
bool *result_sent)
{
if (!state || !work_dbip || !work_wdbp || !staging_dbip ||
!staging_wdbp || !object_name || !result_sent)
return BRLCAD_ERROR;
*result_sent = false;
struct db_i *result_dbip = db_create_inmem();
if (!result_dbip)
return BRLCAD_ERROR;
const char *inputs[] = {object_name};
int ret = _ged_facetize_booleval_tri_to_db(state, work_dbip,
work_wdbp, 1, inputs, FACETIZE_WORKER_RESULT_OBJECT, &rt_vlfree,
result_dbip, 0, 0);
struct rt_db_internal result_internal;
RT_DB_INTERNAL_INIT(&result_internal);
bool result_internal_consumed = false;
struct directory *result_dp = (ret == BRLCAD_OK) ?
db_lookup(result_dbip, FACETIZE_WORKER_RESULT_OBJECT, LOOKUP_QUIET) :
RT_DIR_NULL;
if (!result_dp || result_dp->d_minor_type != ID_BOT ||
rt_db_get_internal(&result_internal, result_dp, result_dbip,
NULL) < 0) {
ret = BRLCAD_ERROR;
}
if (ret == BRLCAD_OK) {
struct rt_bot_internal *result_bot =
(struct rt_bot_internal *)result_internal.idb_ptr;
size_t payload_size = facetize_bot_payload_size(result_bot);
if (!channel.send_region_write_ready(payload_size,
facetize_resident_size(), state->tolerated_failures) ||
!channel.receive_write_proceed() ||
!channel.send_write_started()) {
ret = BRLCAD_ERROR;
} else {
struct directory *old_dp = db_lookup(staging_dbip,
FACETIZE_WORKER_RESULT_OBJECT, LOOKUP_QUIET);
if (old_dp && (db_delete(staging_dbip, old_dp) != 0 ||
db_dirdelete(staging_dbip, old_dp) != 0)) {
ret = BRLCAD_ERROR;
} else {
ret = wdb_put_internal(staging_wdbp,
FACETIZE_WORKER_RESULT_OBJECT, &result_internal,
1.0);
result_internal_consumed = true;
}
if (!channel.send_write_result(ret, facetize_resident_size()))
ret = BRLCAD_ERROR;
else
*result_sent = true;
}
}
if (!result_internal_consumed && result_internal.idb_ptr)
rt_db_free_internal(&result_internal);
db_close(result_dbip);
return ret;
}
static int
facetize_region_server(const char *work_file, const char *source_file,
const char *result_file)
{
if (!work_file || !source_file || !result_file)
return BRLCAD_ERROR;
setvbuf(stdout, NULL, _IONBF, 0);
struct db_i *work_dbip = db_open(work_file, DB_OPEN_READONLY);
struct db_i *source_dbip = db_open(source_file, DB_OPEN_READONLY);
if (!work_dbip || !source_dbip) {
if (work_dbip)
db_close(work_dbip);
if (source_dbip)
db_close(source_dbip);
return BRLCAD_ERROR;
}
if (db_dirbuild(work_dbip) < 0 || db_dirbuild(source_dbip) < 0) {
db_close(source_dbip);
db_close(work_dbip);
return BRLCAD_ERROR;
}
db_update_nref(work_dbip);
struct rt_wdb *work_wdbp = wdb_dbopen(work_dbip,
RT_WDB_TYPE_DB_DEFAULT);
if (!work_wdbp) {
db_close(source_dbip);
db_close(work_dbip);
return BRLCAD_ERROR;
}
(void)bu_file_delete(result_file);
struct db_i *staging_dbip = db_create(result_file,
BRLCAD_DB_FORMAT_LATEST);
struct rt_wdb *staging_wdbp = staging_dbip ?
wdb_dbopen(staging_dbip, RT_WDB_TYPE_DB_DEFAULT) : NULL;
if (!staging_dbip || !staging_wdbp) {
if (staging_dbip)
db_close(staging_dbip);
db_close(source_dbip);
db_close(work_dbip);
return BRLCAD_ERROR;
}
struct bu_vls failure_message = BU_VLS_INIT_ZERO;
struct bu_vls tolerated_failure_log = BU_VLS_INIT_ZERO;
struct _ged_facetize_state state = {};
state.verbosity = -1;
state.failure_msg = &failure_message;
state.tolerated_failure_log = &tolerated_failure_log;
state.dbip = source_dbip;
int server_status = BRLCAD_OK;
FacetizeWorkerServer channel(stdin, stdout);
while (true) {
FacetizeWorkerRequest request;
FacetizeWorkerReadResult read_result =
channel.receive_request(request);
if (read_result == FacetizeWorkerReadResult::End)
break;
if (read_result == FacetizeWorkerReadResult::Error ||
request.operation != FacetizeWorkerOperation::EvaluateRegion ||
request.input_names.size() != 1) {
server_status = BRLCAD_ERROR;
break;
}
const std::string &object_name = request.input_names.front();
state.no_empty = request.region.no_empty;
state.no_fixup = request.region.no_fixup;
state.tolerate_failures = request.region.tolerate_failures;
state.error_flag = 0;
state.facetize_tree = NULL;
state.tolerated_failures = 0;
state.tolerated_failure_details = 0;
state.tolerated_failure_omitted = 0;
bu_vls_trunc(&failure_message, 0);
bu_vls_trunc(&tolerated_failure_log, 0);
bool result_sent = false;
int ret = facetize_region_server_request(&state, work_dbip,
work_wdbp, staging_dbip, staging_wdbp, object_name.c_str(),
channel, &result_sent);
if (ret != BRLCAD_OK && bu_vls_strlen(&failure_message))
bu_log("FACETIZE: region Boolean evaluation failed for %s: %s\n",
object_name.c_str(), bu_vls_cstr(&failure_message));
if (!result_sent && !channel.send_tessellation_result(ret,
facetize_resident_size())) {
server_status = BRLCAD_ERROR;
break;
}
}
bu_vls_free(&tolerated_failure_log);
bu_vls_free(&failure_message);
db_close(staging_dbip);
db_close(source_dbip);
db_close(work_dbip);
return server_status;
}
static int
facetize_writer(const char *work_file)
{
setvbuf(stdout, NULL, _IONBF, 0);
struct db_i *dbip = db_open(work_file, DB_OPEN_READWRITE);
if (!dbip)
return BRLCAD_ERROR;
if (db_dirbuild(dbip) < 0) {
db_close(dbip);
return BRLCAD_ERROR;
}
int writer_status = BRLCAD_OK;
FacetizeWorkerServer channel(stdin, stdout);
while (true) {
FacetizeCommitRequest request;
FacetizeWorkerReadResult read_result = channel.receive_commit(request);
if (read_result == FacetizeWorkerReadResult::End)
break;
if (read_result == FacetizeWorkerReadResult::Error ||
!channel.send_write_ready(request.payload_size,
facetize_resident_size())) {
writer_status = BRLCAD_ERROR;
break;
}
facetize_test_fault(FACETIZE_TEST_FAULT_WRITER_PROCEED,
request.object_name.c_str());
if (!channel.receive_write_proceed()) {
writer_status = BRLCAD_ERROR;
break;
}
if (!channel.send_write_started()) {
writer_status = BRLCAD_ERROR;
break;
}
int ret = facetize_transfer_staged_object(dbip,
request.result_file.c_str(), request.object_name.c_str(), ID_BOT);
if (ret == BRLCAD_OK)
facetize_test_fault(FACETIZE_TEST_FAULT_WRITER_DONE,
request.object_name.c_str());
if (!channel.send_write_result(ret, facetize_resident_size())) {
writer_status = BRLCAD_ERROR;
break;
}
}
db_close(dbip);
return writer_status;
}
static int
facetize_nmg_server(const char *source_file, const char *result_file)
{
if (!source_file || !result_file)
return BRLCAD_ERROR;
setvbuf(stdout, NULL, _IONBF, 0);
struct ged *gedp = ged_open("db", source_file, 1);
if (!gedp)
return BRLCAD_ERROR;
struct db_i *result_dbip = db_create(result_file,
BRLCAD_DB_FORMAT_LATEST);
if (!result_dbip) {
ged_close(gedp);
return BRLCAD_ERROR;
}
FacetizeWorkerServer channel(stdin, stdout);
FacetizeWorkerRequest request;
FacetizeWorkerReadResult read_result = channel.receive_request(request);
int server_status = BRLCAD_OK;
FacetizeNmgBooleanResult *result = NULL;
int ret = (read_result == FacetizeWorkerReadResult::Request) ?
facetize_nmg_boolean_evaluate(gedp, request, &result) : BRLCAD_ERROR;
if (read_result != FacetizeWorkerReadResult::Request) {
server_status = BRLCAD_ERROR;
} else if (ret == BRLCAD_OK) {
size_t payload_size = facetize_nmg_boolean_payload_size(result);
facetize_test_fault(FACETIZE_TEST_FAULT_NMG_READY,
request.output_name.c_str());
if (!channel.send_write_ready(payload_size,
facetize_resident_size()) ||
!channel.receive_write_proceed() ||
!channel.send_write_started()) {
server_status = BRLCAD_ERROR;
} else {
ret = facetize_nmg_boolean_write(result_dbip, result);
if (!channel.send_write_result(ret, facetize_resident_size()))
server_status = BRLCAD_ERROR;
}
} else if (!channel.send_tessellation_result(ret,
facetize_resident_size())) {
server_status = BRLCAD_ERROR;
}
facetize_nmg_boolean_destroy(result);
db_close(result_dbip);
ged_close(gedp);
return server_status;
}
@@ -465,10 +823,16 @@ facetize_process(int argc, const char **argv)
int list_methods = 0;
int server_mode = 0;
int writer_mode = 0;
int validation_server_mode = 0;
int region_server_mode = 0;
int nmg_server_mode = 0;
int max_time = 0;
int max_pnts = 0;
int worker_threads = 0;
struct bu_vls result_file = BU_VLS_INIT_ZERO;
struct bu_opt_desc d[10];
struct bu_opt_desc d[16];
BU_OPT(d[ 0], "h", "help", "", NULL, &print_help, "Print help and exit");
BU_OPT(d[ 1], "", "list-methods", "", NULL, &list_methods, "List available tessellation methods. When used with -h, print an informational summary of each method.");
BU_OPT(d[ 2], "O", "overwrite", "", NULL, &(s.overwrite_obj), "Replace original object with BoT");
@@ -477,8 +841,14 @@ facetize_process(int argc, const char **argv)
BU_OPT(d[ 5], "", "max-time", "#", &bu_opt_int, &max_time, "Maximum number of seconds to allow for runtime (not supported by all methods).");
BU_OPT(d[ 6], "", "max-pnts", "#", &bu_opt_int, &max_pnts, "Maximum number of pnts to use when applying ray sampling methods.");
BU_OPT(d[ 7], "", "cache-dir", "dir", &bu_opt_vls, &cache_dir, "Directory to use for cached outputs (default is libbu cache directory).");
BU_OPT(d[ 8], "", "server", "", NULL, &server_mode, "Run as a persistent worker, overwriting requested objects in the working database.");
BU_OPT_NULL(d[ 9]);
BU_OPT(d[ 8], "", "server", "", NULL, &server_mode, "Run as a persistent worker.");
BU_OPT(d[ 9], "", "threads", "#", &bu_opt_int, &worker_threads, "Maximum CPU threads available to this process.");
BU_OPT(d[10], "", "result-file", "file.g", &bu_opt_vls, &result_file, "Write server results to a staging database.");
BU_OPT(d[11], "", "writer", "", NULL, &writer_mode, "Run as a persistent staged-result writer.");
BU_OPT(d[12], "", "validation-server", "", NULL, &validation_server_mode, "Run as a persistent CSG validation worker.");
BU_OPT(d[13], "", "region-server", "", NULL, &region_server_mode, "Run as a persistent region Boolean worker.");
BU_OPT(d[14], "", "nmg-server", "", NULL, &nmg_server_mode, "Run one isolated NMG Boolean request.");
BU_OPT_NULL(d[15]);
/* parse options */
struct bu_vls omsg = BU_VLS_INIT_ZERO;
@@ -490,13 +860,33 @@ facetize_process(int argc, const char **argv)
}
bu_vls_free(&omsg);
if (worker_threads < 0 || worker_threads > MAX_PSW) {
bu_vls_free(&cache_dir);
bu_vls_free(&result_file);
return BRLCAD_ERROR;
}
if (worker_threads)
bu_avail_cpus_set((size_t)worker_threads);
int worker_mode_count = server_mode + writer_mode +
validation_server_mode + region_server_mode + nmg_server_mode;
if (worker_mode_count > 1) {
bu_vls_free(&cache_dir);
bu_vls_free(&result_file);
return BRLCAD_ERROR;
}
if (list_methods && print_help) {
print_methods_info();
bu_vls_free(&cache_dir);
bu_vls_free(&result_file);
return BRLCAD_OK;
}
if (list_methods) {
print_tess_methods();
bu_vls_free(&cache_dir);
bu_vls_free(&result_file);
return BRLCAD_OK;
}
@@ -513,9 +903,55 @@ facetize_process(int argc, const char **argv)
bu_log("%s\n", bu_vls_cstr(&str));
bu_vls_free(&str);
bu_vls_free(&cache_dir);
bu_vls_free(&result_file);
return BRLCAD_OK;
}
if (writer_mode) {
int ret = (argc == 1 && !bu_vls_strlen(&result_file)) ?
facetize_writer(argv[0]) : BRLCAD_ERROR;
bu_vls_free(&cache_dir);
bu_vls_free(&result_file);
return ret;
}
if (validation_server_mode) {
int ret = (argc == 1 && !bu_vls_strlen(&result_file)) ?
facetize_validation_server(argv[0]) : BRLCAD_ERROR;
bu_vls_free(&cache_dir);
bu_vls_free(&result_file);
return ret;
}
if (region_server_mode) {
int ret = (argc == 2 && bu_vls_strlen(&result_file)) ?
facetize_region_server(argv[0], argv[1],
bu_vls_cstr(&result_file)) : BRLCAD_ERROR;
bu_vls_free(&cache_dir);
bu_vls_free(&result_file);
return ret;
}
if (nmg_server_mode) {
int ret = (argc == 1 && bu_vls_strlen(&result_file)) ?
facetize_nmg_server(argv[0], bu_vls_cstr(&result_file)) :
BRLCAD_ERROR;
bu_vls_free(&cache_dir);
bu_vls_free(&result_file);
return ret;
}
if (server_mode) {
int ret = (argc == 1) ?
facetize_server(argv[0], bu_vls_strlen(&result_file) ?
bu_vls_cstr(&result_file) : NULL) :
BRLCAD_ERROR;
bu_vls_free(&cache_dir);
bu_vls_free(&result_file);
return ret;
}
// If we have a non-default cache directory specified, set it up
if (bu_vls_strlen(&cache_dir)) {
// Make sure it's there first
@@ -524,19 +960,13 @@ facetize_process(int argc, const char **argv)
bu_setenv("BU_DIR_CACHE", bu_vls_cstr(&cache_dir), 1);
}
// Do the setup for the various methods
// Do the setup for the various tessellation methods.
method_setup(&s);
if (server_mode) {
int ret = (argc == 1 && s.overwrite_obj) ?
facetize_server(argv[0], &s) : BRLCAD_ERROR;
bu_vls_free(&cache_dir);
return ret;
}
if (argc < 2) {
bu_log("%s", usage);
bu_vls_free(&cache_dir);
bu_vls_free(&result_file);
return BRLCAD_ERROR;
}
@@ -544,6 +974,7 @@ facetize_process(int argc, const char **argv)
struct ged *gedp = ged_open("db", argv[0], 1);
if (!gedp) {
bu_vls_free(&cache_dir);
bu_vls_free(&result_file);
return BRLCAD_ERROR;
}
@@ -555,6 +986,7 @@ facetize_process(int argc, const char **argv)
bu_ptbl_free(&dps);
ged_close(gedp);
bu_vls_free(&cache_dir);
bu_vls_free(&result_file);
return BRLCAD_ERROR;
}
bu_ptbl_ins(&dps, (long *)dp);
@@ -607,6 +1039,7 @@ facetize_process(int argc, const char **argv)
bu_ptbl_free(&dps);
ged_close(gedp);
bu_vls_free(&cache_dir);
bu_vls_free(&result_file);
return process_ret;
}
+137
View File
@@ -0,0 +1,137 @@
/* M D C . C P P
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this file; see the file named COPYING for more
* information.
*/
/** @file mdc.cpp
*
* Facetize adapter for libanalyze's ray-driven manifold dual contourer.
*/
#include "common.h"
#include "analyze/contour.h"
#include "bu/log.h"
#include "rt/primitives/bot.h"
#include "./tessellate.h"
#include "../validation.h"
static const char *
mdc_status_message(enum analyze_mdc_status status)
{
switch (status) {
case ANALYZE_MDC_OK:
return "success";
case ANALYZE_MDC_INVALID_INPUT:
return "invalid settings or input";
case ANALYZE_MDC_PREP_FAILED:
return "raytrace preparation failed";
case ANALYZE_MDC_NO_SURFACE:
return "no closed surface was sampled";
case ANALYZE_MDC_RAY_LIMIT:
return "ray limit reached";
case ANALYZE_MDC_TIMEOUT:
return "time limit reached";
case ANALYZE_MDC_MEMORY_LIMIT:
return "free-memory limit reached";
case ANALYZE_MDC_AMBIGUOUS:
return "surface remained ambiguous at the maximum depth";
case ANALYZE_MDC_NOT_MANIFOLD:
return "generated mesh was not a closed oriented manifold";
}
return "unknown error";
}
int
mdc_mesh(struct rt_bot_internal **output, struct db_i *dbip,
const char *object, tess_opts *settings)
{
if (!output || !dbip || !object || !settings)
return BRLCAD_ERROR;
*output = NULL;
const mdc_opts &options = settings->mdc_options;
if (!options.invalid_option.empty()) {
bu_log("MDC: invalid option for %s: %s\n", object,
options.invalid_option.c_str());
return BRLCAD_ERROR;
}
struct analyze_mdc_params params = ANALYZE_MDC_PARAMS_DEFAULT;
params.feature_size = options.feature_size;
params.max_rays = static_cast<size_t>(options.max_rays);
params.minimum_free_mem =
static_cast<size_t>(options.minimum_free_mem);
params.min_depth = options.min_depth;
params.max_depth = options.max_depth;
params.max_time = options.max_time;
params.verbosity = options.verbosity;
int *faces = NULL;
point_t *vertices = NULL;
size_t face_count = 0;
size_t vertex_count = 0;
struct bu_hook_list saved_hooks = BU_HOOK_LIST_INIT_ZERO;
bool silence_logs = (options.verbosity == 0);
if (silence_logs)
facetize_log_hooks_silence(&saved_hooks);
enum analyze_mdc_status status = analyze_mdc(&faces, &face_count,
&vertices, &vertex_count, object, dbip, &params);
if (silence_logs)
facetize_log_hooks_restore(&saved_hooks);
if (status != ANALYZE_MDC_OK) {
bu_log("MDC: failed to tessellate %s: %s\n", object,
mdc_status_message(status));
return BRLCAD_ERROR;
}
struct rt_bot_internal *bot;
BU_ALLOC(bot, struct rt_bot_internal);
bot->magic = RT_BOT_INTERNAL_MAGIC;
bot->mode = RT_BOT_SOLID;
bot->orientation = RT_BOT_CCW;
bot->num_faces = face_count;
bot->num_vertices = vertex_count;
bot->faces = faces;
bot->vertices = reinterpret_cast<fastf_t *>(vertices);
if (!bot_is_manifold(bot)) {
bu_log("MDC: generated mesh for %s is not accepted by Manifold\n",
object);
_tess_facetize_free_bot(bot);
return BRLCAD_ERROR;
}
*output = bot;
return BRLCAD_OK;
}
/*
* Local Variables:
* mode: C++
* tab-width: 8
* c-basic-offset: 4
* indent-tabs-mode: t
* c-file-style: "stroustrup"
* End:
* ex: shiftwidth=4 tabstop=8
*/
+2 -1
View File
@@ -42,7 +42,8 @@ _nmg_tessellate(struct rt_bot_internal **nbot, struct rt_db_internal *intern, te
int ret = BRLCAD_ERROR;
struct bu_list *vlfree = &rt_vlfree;
if (!nbot || !intern || !intern->idb_meth || !s)
if (!nbot || !intern || !intern->idb_meth || !s ||
!intern->idb_meth->ft_tessellate)
return BRLCAD_ERROR;
// TODO - get the nmg debug option from nmg_options and set it
@@ -0,0 +1,279 @@
/* N M G _ B O O L E A N . C P P
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this file; see the file named COPYING for more
* information.
*/
/** @file libged/facetize/subprocess/nmg_boolean.cpp
*
* Isolated NMG tree evaluation for the facetize worker. An NMG bomb may
* invalidate the model's topology, so a bombed request intentionally leaves
* its allocations to process teardown rather than risking another bomb while
* attempting cleanup.
*/
#include "common.h"
#include <string>
#include <vector>
#include "raytrace.h"
#include "wdb.h"
#include "../../ged_private.h"
#include "../ged_facetize.h"
#include "./nmg_boolean.h"
struct FacetizeNmgBooleanResult {
struct model *nmg_model = NULL;
struct rt_bot_internal *bot = NULL;
};
struct FacetizeNmgBooleanState {
union tree *tree = NULL;
};
static union tree *
facetize_nmg_region_end(struct db_tree_state *tsp,
const struct db_full_path *pathp, union tree *current_tree,
void *client_data)
{
if (tsp)
RT_CK_DBTS(tsp);
if (pathp)
RT_CK_FULL_PATH(pathp);
FacetizeNmgBooleanState *state =
static_cast<FacetizeNmgBooleanState *>(client_data);
if (!state || !current_tree || current_tree->tr_op == OP_NOP)
return current_tree;
if (!state->tree) {
state->tree = current_tree;
return TREE_NULL;
}
union tree *union_tree;
BU_ALLOC(union_tree, union tree);
RT_TREE_INIT(union_tree);
union_tree->tr_op = OP_UNION;
union_tree->tr_b.tb_regionp = REGION_NULL;
union_tree->tr_b.tb_left = state->tree;
union_tree->tr_b.tb_right = current_tree;
state->tree = union_tree;
return TREE_NULL;
}
static union tree *
facetize_nmg_leaf(struct db_tree_state *tsp,
const struct db_full_path *pathp, struct rt_db_internal *ip,
void *UNUSED(client_data))
{
union tree *leaf = rt_booltree_leaf_tess(tsp, pathp, ip, NULL);
if (!leaf && pathp) {
char *path = db_path_to_string(pathp);
bu_log("NMG leaf tessellation failed for '%s'\n",
path ? path : "(unknown)");
if (path)
bu_free(path, "NMG leaf path");
}
return leaf;
}
static int
facetize_nmg_compute(struct ged *gedp,
const FacetizeWorkerRequest &request,
FacetizeNmgBooleanResult *result)
{
struct rt_wdb *wdbp = wdb_dbopen(gedp->dbip, RT_WDB_TYPE_DB_DEFAULT);
if (!wdbp)
return BRLCAD_ERROR;
struct db_tree_state initial_state;
db_init_db_tree_state(&initial_state, gedp->dbip);
initial_state.ts_ttol = &wdbp->wdb_ttol;
initial_state.ts_tol = &wdbp->wdb_tol;
struct model *nmg_model = nmg_mm();
initial_state.ts_m = &nmg_model;
FacetizeNmgBooleanState state;
std::vector<const char *> input_argv;
input_argv.reserve(request.input_names.size());
for (const std::string &input_name : request.input_names)
input_argv.push_back(input_name.c_str());
int walk_result = BRLCAD_ERROR;
if (!BU_SETJUMP) {
walk_result = db_walk_tree(gedp->dbip, (int)input_argv.size(),
input_argv.data(), 1, &initial_state, NULL,
facetize_nmg_region_end, facetize_nmg_leaf, &state);
} else {
BU_UNSETJUMP;
bu_log("NMG tree tessellation bombed while processing %s\n",
request.output_name.c_str());
return BRLCAD_ERROR;
}
BU_UNSETJUMP;
if (walk_result < 0 || !state.tree) {
if (state.tree)
db_free_tree(state.tree);
nmg_km(nmg_model);
return BRLCAD_ERROR;
}
int boolean_result = BRLCAD_ERROR;
if (!BU_SETJUMP) {
boolean_result = nmg_boolean(state.tree, nmg_model, &rt_vlfree,
&wdbp->wdb_tol);
if (boolean_result == 0) {
NMG_CK_REGION(state.tree->tr_d.td_r);
state.tree->tr_d.td_r = NULL;
}
} else {
BU_UNSETJUMP;
bu_log("NMG Boolean evaluation bombed while processing %s\n",
request.output_name.c_str());
return BRLCAD_ERROR;
}
BU_UNSETJUMP;
if (boolean_result != 0) {
db_free_tree(state.tree);
nmg_km(nmg_model);
return BRLCAD_ERROR;
}
db_free_tree(state.tree);
if (request.operation == FacetizeWorkerOperation::NmgBooleanToNmg) {
result->nmg_model = nmg_model;
return BRLCAD_OK;
}
struct rt_bot_internal *bot = NULL;
if (!BU_SETJUMP) {
bot = static_cast<struct rt_bot_internal *>(
nmg_mdl_to_bot(nmg_model, &rt_vlfree, &wdbp->wdb_tol));
} else {
BU_UNSETJUMP;
bu_log("NMG-to-BoT conversion bombed while processing %s\n",
request.output_name.c_str());
return BRLCAD_ERROR;
}
BU_UNSETJUMP;
nmg_km(nmg_model);
if (!bot)
return BRLCAD_ERROR;
result->bot = bot;
return BRLCAD_OK;
}
int
facetize_nmg_boolean_evaluate(struct ged *gedp,
const FacetizeWorkerRequest &request,
FacetizeNmgBooleanResult **result)
{
if (!gedp || !result || !request.valid() ||
(request.operation != FacetizeWorkerOperation::NmgBooleanToBot &&
request.operation != FacetizeWorkerOperation::NmgBooleanToNmg))
return BRLCAD_ERROR;
*result = NULL;
FacetizeNmgBooleanResult *evaluation =
new FacetizeNmgBooleanResult;
if (facetize_nmg_compute(gedp, request, evaluation) != BRLCAD_OK) {
/* Bomb paths deliberately abandon unsafe NMG allocations until this
* one-shot worker exits. */
facetize_nmg_boolean_destroy(evaluation);
return BRLCAD_ERROR;
}
*result = evaluation;
return BRLCAD_OK;
}
size_t
facetize_nmg_boolean_payload_size(
const FacetizeNmgBooleanResult *UNUSED(result))
{
/* NMG model storage is graph-shaped and has no cheap serialized-size
* estimate. The supervisor applies its conservative default write
* timeout when the estimate is zero. */
return 0;
}
int
facetize_nmg_boolean_write(struct db_i *dbip,
FacetizeNmgBooleanResult *result)
{
if (!dbip || !result || (!result->bot && !result->nmg_model))
return BRLCAD_ERROR;
struct rt_db_internal intern;
RT_DB_INTERNAL_INIT(&intern);
intern.idb_major_type = DB5_MAJORTYPE_BRLCAD;
intern.idb_type = result->bot ? ID_BOT : ID_NMG;
intern.idb_meth = &OBJ[intern.idb_type];
intern.idb_ptr = result->bot ? static_cast<void *>(result->bot) :
static_cast<void *>(result->nmg_model);
bu_avs_init_empty(&intern.idb_avs);
(void)bu_avs_add(&intern.idb_avs, "facetized", "1");
(void)bu_avs_add(&intern.idb_avs, FACETIZE_METHOD_ATTR, "NMG Boolean");
struct directory *output = db_diradd(dbip,
FACETIZE_WORKER_RESULT_OBJECT, RT_DIR_PHONY_ADDR, 0,
RT_DIR_SOLID, &intern.idb_type);
if (!output) {
rt_db_free_internal(&intern);
result->bot = NULL;
result->nmg_model = NULL;
return BRLCAD_ERROR;
}
int write_result = rt_db_put_internal(output, dbip, &intern);
result->bot = NULL;
result->nmg_model = NULL;
return write_result < 0 ? BRLCAD_ERROR : BRLCAD_OK;
}
void
facetize_nmg_boolean_destroy(FacetizeNmgBooleanResult *result)
{
if (!result)
return;
if (result->bot) {
struct rt_db_internal intern;
RT_DB_INTERNAL_INIT(&intern);
intern.idb_major_type = DB5_MAJORTYPE_BRLCAD;
intern.idb_type = ID_BOT;
intern.idb_meth = &OBJ[ID_BOT];
intern.idb_ptr = result->bot;
rt_db_free_internal(&intern);
}
if (result->nmg_model)
nmg_km(result->nmg_model);
delete result;
}
// Local Variables:
// tab-width: 8
// mode: C++
// c-basic-offset: 4
// indent-tabs-mode: t
// c-file-style: "stroustrup"
// End:
// ex: shiftwidth=4 tabstop=8
@@ -0,0 +1,41 @@
/* N M G _ B O O L E A N . H
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this file; see the file named COPYING for more
* information.
*/
#ifndef FACETIZE_PROCESS_NMG_BOOLEAN_H
#define FACETIZE_PROCESS_NMG_BOOLEAN_H
#include <cstddef>
#include "../worker.h"
struct db_i;
struct ged;
struct FacetizeNmgBooleanResult;
int facetize_nmg_boolean_evaluate(struct ged *gedp,
const FacetizeWorkerRequest &request,
FacetizeNmgBooleanResult **result);
size_t facetize_nmg_boolean_payload_size(
const FacetizeNmgBooleanResult *result);
int facetize_nmg_boolean_write(struct db_i *dbip,
FacetizeNmgBooleanResult *result);
void facetize_nmg_boolean_destroy(FacetizeNmgBooleanResult *result);
#endif /* FACETIZE_PROCESS_NMG_BOOLEAN_H */
-177
View File
@@ -1,177 +0,0 @@
/* S A M P L E . C P P
* BRL-CAD
*
* Copyright (c) 2008-2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this file; see the file named COPYING for more
* information.
*/
/** @file libged/facetize/continuation.cpp
*
* The facetize command's Continuation Method implementation.
*/
#include "common.h"
#include "vmath.h"
#include "bu/datetime.h"
#include "bg/trimesh.h"
#include "raytrace.h"
#include "analyze/polygonize.h"
#include "../../ged_private.h"
#include "./tessellate.h"
#define FACETIZE_MEMORY_THRESHOLD 150000000
static void
_rt_pnts_bbox(point_t rpp_min, point_t rpp_max, struct rt_pnts_internal *pnts)
{
struct pnt_normal *pn = NULL;
struct pnt_normal *pl = NULL;
if (!rpp_min || !rpp_max || !pnts || !pnts->point) return;
VSETALL(rpp_min, INFINITY);
VSETALL(rpp_max, -INFINITY);
pl = (struct pnt_normal *)pnts->point;
for (BU_LIST_FOR(pn, pnt_normal, &(pl->l))) {
VMINMAX(rpp_min, rpp_max, pn->v);
}
}
static double
_bbox_vol(point_t b_min, point_t b_max)
{
double bbox_vol = 0.0;
fastf_t b_xlen, b_ylen, b_zlen;
b_xlen = fabs(b_max[X] - b_min[X]);
b_ylen = fabs(b_max[Y] - b_min[Y]);
b_zlen = fabs(b_max[Z] - b_min[Z]);
bbox_vol = b_xlen * b_ylen * b_zlen;
return bbox_vol;
}
struct rt_pnts_internal *
_tess_pnts_sample(const char *oname, struct db_i *dbip, tess_opts *s)
{
if (!oname || !dbip || !s)
return NULL;
struct directory *dp = db_lookup(dbip, oname, LOOKUP_QUIET);
if (!dp)
return NULL;
/* If we have a point cloud or half, this won't work */
if (dp->d_minor_type == DB5_MINORTYPE_BRLCAD_PNTS || dp->d_minor_type == DB5_MINORTYPE_BRLCAD_HALF)
return NULL;
/* Key some settings off the bbox size */
point_t obj_min, obj_max;
struct bu_vls msg = BU_VLS_INIT_ZERO;
rt_obj_bounds(&msg, dbip, 1, (const char **)&oname, 0, obj_min, obj_max);
if (bu_vls_strlen(&msg))
bu_log("Bounds calculation: %s\n", bu_vls_cstr(&msg));
bu_vls_free(&msg);
point_t rpp_min, rpp_max;
VSETALL(rpp_min, INFINITY);
VSETALL(rpp_max, -INFINITY);
VMINMAX(rpp_min, rpp_max, (double *)obj_min);
VMINMAX(rpp_min, rpp_max, (double *)obj_max);
double xlen = fabs(rpp_max[X] - rpp_min[X]);
double ylen = fabs(rpp_max[Y] - rpp_min[Y]);
double zlen = fabs(rpp_max[Z] - rpp_min[Z]);
/* Pick our mode(s) */
int flags = 0;
flags |= RT_GEN_OBJ_PNTS_RAND;
//flags |= RT_GEN_OBJ_PNTS_SOBOL;
struct rt_pnts_internal *pnts = NULL;
BU_GET(pnts, struct rt_pnts_internal);
pnts->magic = RT_PNTS_INTERNAL_MAGIC;
pnts->scale = 0.0;
pnts->count = 0;
pnts->type = RT_PNT_TYPE_NRM;
pnts->point = NULL;
/* Shoot - we need both the avg thickness of the hit partitions and seed points */
struct bn_tol btol = BN_TOL_INIT_TOL;
if (rt_gen_obj_pnts(pnts, &s->pnt_options.avg_thickness, dbip, oname, &btol, flags, s->pnt_options.max_pnts, s->pnt_options.max_sample_time, 1) || pnts->count <= 0) {
struct pnt_normal *rpnt = (struct pnt_normal *)pnts->point;
if (rpnt) {
struct pnt_normal *entry;
while (BU_LIST_WHILE(entry, pnt_normal, &(rpnt->l))) {
BU_LIST_DEQUEUE(&(entry->l));
BU_PUT(entry, struct pnt_normal);
}
BU_PUT(rpnt, struct pnt_normal);
}
BU_PUT(pnts, struct rt_pnts_internal);
return NULL;
}
/* Check the volume of the bounding box of input object against the bounding box
* of the point cloud - a large difference means something probably isn't
* right. This one we are a bit more generous with, since the parent bbox
* may not be terribly tight. */
{
point_t p_min, p_max;
VSETALL(p_min, INFINITY);
VSETALL(p_max, -INFINITY);
_rt_pnts_bbox(p_min, p_max, pnts);
s->pnt_options.pnts_bbox_vol = _bbox_vol(p_min, p_max);
s->pnt_options.obj_bbox_vol = _bbox_vol(rpp_min, rpp_max);
if (fabs(s->pnt_options.obj_bbox_vol - s->pnt_options.pnts_bbox_vol)/s->pnt_options.obj_bbox_vol > 1) {
struct pnt_normal *rpnt = (struct pnt_normal *)pnts->point;
if (rpnt) {
struct pnt_normal *entry;
while (BU_LIST_WHILE(entry, pnt_normal, &(rpnt->l))) {
BU_LIST_DEQUEUE(&(entry->l));
BU_PUT(entry, struct pnt_normal);
}
BU_PUT(rpnt, struct pnt_normal);
}
BU_PUT(pnts, struct rt_pnts_internal);
return NULL;
}
}
/* Find the smallest value from either the bounding box lengths or the avg
* thickness observed by the rays. Some fraction of this value is our box
* size for the polygonizer and the decimation routine */
double min_len = (xlen < ylen) ? xlen : ylen;
min_len = (min_len < zlen) ? min_len : zlen;
min_len = (min_len < s->pnt_options.avg_thickness) ? min_len : s->pnt_options.avg_thickness;
if (s->pnt_options.feature_size > 0) {
s->pnt_options.target_feature_size = 0.5*s->pnt_options.feature_size;
} else {
s->pnt_options.target_feature_size = min_len * s->pnt_options.feature_scale;
}
bu_log("feature_size: %f\n", s->pnt_options.feature_size);
bu_log("feature_scale: %f\n", s->pnt_options.feature_scale);
bu_log("target_feature_size: %f\n", s->pnt_options.target_feature_size);
return pnts;
}
// Local Variables:
// tab-width: 8
// mode: C++
// c-basic-offset: 4
// indent-tabs-mode: t
// c-file-style: "stroustrup"
// End:
// ex: shiftwidth=4 tabstop=8
File diff suppressed because it is too large Load Diff
+6 -7
View File
@@ -41,11 +41,10 @@ class tess_opts {
public:
method_options_t method_opts;
nmg_opts nmg_options;
cm_opts cm_options;
mdc_opts mdc_options;
spsr_opts spsr_options;
int overwrite_obj = 0;
sample_opts pnt_options; // Values used by sample.cpp
};
extern struct rt_bot_internal *
@@ -57,9 +56,6 @@ _tess_facetize_write_bot(struct db_i *dbip, struct rt_bot_internal *bot, const c
extern void
_tess_facetize_free_bot(struct rt_bot_internal *bot);
extern struct rt_pnts_internal *
_tess_pnts_sample(const char *oname, struct db_i *dbip, tess_opts *s);
extern int
_brep_csg_tessellate(struct rt_bot_internal **obot, struct ged *gedp,
struct directory *dp, tess_opts *s);
@@ -68,10 +64,13 @@ extern int
_nmg_tessellate(struct rt_bot_internal **nbot, struct rt_db_internal *intern, tess_opts *s);
extern int
continuation_mesh(struct rt_bot_internal **obot, struct db_i *dbip, const char *objname, tess_opts *s, point_t seed);
mdc_mesh(struct rt_bot_internal **output, struct db_i *dbip,
const char *object, tess_opts *settings);
extern int
spsr_mesh(struct rt_bot_internal **obot, struct db_i *dbip, struct rt_pnts_internal *pnts, tess_opts *s);
spsr_mesh(struct rt_bot_internal **obot, struct db_i *dbip,
const char *object, struct rt_pnts_internal *pnts,
bool best_effort_source, tess_opts *s);
extern bool
bot_is_manifold(struct rt_bot_internal *bot);
+165 -134
View File
@@ -39,6 +39,7 @@
#include "bn/tol.h"
#include "bg/defines.h"
#include "bg/spsr.h"
#include "analyze/contour.h"
#include "raytrace.h"
class method_options_t {
@@ -63,6 +64,13 @@ class method_options_t {
std::string method_optstr(std::string &method, struct db_i *dbip);
};
inline const std::vector<std::string> &
tess_default_methods()
{
static const std::vector<std::string> methods = {"NMG", "MDC", "SPSR"};
return methods;
}
int _tess_active_methods(struct bu_vls *msg, size_t argc, const char **argv, void *set_var);
int _tess_method_opts(struct bu_vls *msg, size_t argc, const char **argv, void *set_var);
@@ -111,18 +119,24 @@ class nmg_opts : public method_opts {
int plate_max_time = 1200;
};
class cm_opts : public sample_opts {
class mdc_opts : public method_opts {
public:
std::string about_method();
std::string print_options_help();
int set_var(const std::string &key, const std::string &val);
void sync(method_options_t &mopts);
void sync(sample_opts &sopts);
int max_cycle_time = 30; // Maximum length of a processing cycle to allow before finalizing
int max_time = 600; // Maximum overall time
fastf_t feature_size = 0.0;
long max_rays = ANALYZE_MDC_DEFAULT_MAX_RAYS;
long minimum_free_mem = ANALYZE_MDC_DEFAULT_MINIMUM_FREE_MEM;
int min_depth = ANALYZE_MDC_DEFAULT_MIN_DEPTH;
int max_depth = ANALYZE_MDC_DEFAULT_MAX_DEPTH;
int max_time = ANALYZE_MDC_DEFAULT_MAX_TIME;
int verbosity = 0;
std::string invalid_option;
};
class spsr_opts : public sample_opts {
public:
std::string about_method();
@@ -132,10 +146,8 @@ class spsr_opts : public sample_opts {
void sync(sample_opts &sopts);
struct bg_3d_spsr_opts s_opts = BG_3D_SPSR_OPTS_DEFAULT;
int depth = 8; // Maximum reconstruction depth s_opts.depth
fastf_t interpolate = 2.0; // Lower values (down to 0.0) bias towards a smoother mesh, higher values bias towards interpolation accuracy. s_opts.point_weight
fastf_t samples_per_node = 1.5; // How many samples should go into a cell before it is refined. s_opts.samples_per_node
int max_time = 600; // Maximum overall time
int refinement_passes = BG_3D_SPSR_DEFAULT_REFINEMENT_PASSES;
int max_time = 600;
};
@@ -146,11 +158,11 @@ class spsr_opts : public sample_opts {
method_options_t::method_options_t()
{
nmg_opts n;
cm_opts cm;
mdc_opts mdc;
spsr_opts s;
max_time[std::string("NMG")] = n.max_time;
max_time[std::string("CM")] = cm.max_time;
max_time[std::string("MDC")] = mdc.max_time;
max_time[std::string("SPSR")] = s.max_time;
plate_max_time = n.plate_max_time;
}
@@ -296,52 +308,36 @@ sample_opts::print_options_help()
int
sample_opts::set_var(const std::string &key, const std::string &val)
{
if (key.length() == 0)
if (key.empty())
return BRLCAD_ERROR;
const char *cstr[2];
cstr[0] = val.c_str();
cstr[1] = NULL;
const char *value[2] = {val.c_str(), NULL};
if (key == "feature_scale" || key == "feature_size" ||
key == "d_feature_size") {
fastf_t parsed = 0.0;
if ((!val.empty() &&
bu_opt_fastf_t(NULL, 1, value, &parsed) < 0) ||
parsed < 0.0)
return BRLCAD_ERROR;
if (key == "feature_scale")
feature_scale = parsed;
else if (key == "feature_size")
feature_size = parsed;
else
d_feature_size = parsed;
return BRLCAD_OK;
}
if (key == std::string("feature_scale")) {
if (!val.length()) {
feature_scale = 0.0;
return BRLCAD_OK;
}
if (bu_opt_fastf_t(NULL, 1, (const char **)cstr, (void *)&feature_scale) < 0)
return BRLCAD_ERROR;
}
if (key == std::string("feature_size")) {
if (!val.length()) {
feature_size = 0.0;
return BRLCAD_OK;
}
if (bu_opt_fastf_t(NULL, 1, (const char **)cstr, (void *)&feature_size) < 0)
return BRLCAD_ERROR;
}
if (key == std::string("d_feature_size")) {
if (!val.length()) {
d_feature_size = 0.0;
return BRLCAD_OK;
}
if (bu_opt_fastf_t(NULL, 1, (const char **)cstr, (void *)&d_feature_size) < 0)
return BRLCAD_ERROR;
}
if (key == std::string("max_sample_time")) {
if (!val.length()) {
max_sample_time = 0;
return BRLCAD_OK;
}
if (bu_opt_int(NULL, 1, (const char **)cstr, (void *)&max_sample_time) < 0)
return BRLCAD_ERROR;
}
if (key == std::string("max_pnts")) {
if (!val.length()) {
max_pnts = 0;
return BRLCAD_OK;
}
if (bu_opt_int(NULL, 1, (const char **)cstr, (void *)&max_pnts) < 0)
if (key == "max_sample_time" || key == "max_pnts") {
int parsed = 0;
if ((!val.empty() && bu_opt_int(NULL, 1, value, &parsed) < 0) ||
parsed < 0)
return BRLCAD_ERROR;
if (key == "max_sample_time")
max_sample_time = parsed;
else
max_pnts = parsed;
return BRLCAD_OK;
}
return BRLCAD_ERROR;
@@ -379,77 +375,104 @@ sample_opts::equals(sample_opts &o)
}
std::string
cm_opts::about_method()
mdc_opts::about_method()
{
std::string msg = "Continuation Method (Bloomenthal polygonizer)\n";
return msg;
return std::string("Ray-driven Manifold Dual Contouring\n");
}
std::string
cm_opts::print_options_help()
mdc_opts::print_options_help()
{
std::string h = std::string("Continuation (CM) Method Options:\n") + sample_opts::print_options_help();
h.append("max_cycle_time - Maximum time to take for one processing cycle\n");
h.append(" Default is 30. Zero means run until the target\n");
h.append(" size is met or other termination criteria kick\n");
h.append(" in. Be careful when specifying zero - it can\n");
h.append(" produce very long runs!\n");
h.append("max_time - Maximum overall run time for object conversion\n");
return h;
std::string help;
help.append("Manifold Dual Contouring (MDC) options:\n");
help.append("feature_size - Maximum finest-cell edge length. Zero selects\n");
help.append(" an automatic size from the object bounds.\n");
help.append("max_rays - Optional ray-budget ceiling. Zero uses automatic\n");
help.append(" geometry and system resource limits.\n");
help.append("minimum_free_mem - Stop before free memory falls below this many bytes.\n");
help.append("min_depth - Minimum power-of-two grid depth.\n");
help.append("max_depth - Maximum power-of-two grid depth.\n");
help.append("max_time - Maximum run time in seconds. Zero is unlimited.\n");
help.append("verbosity - Emit diagnostics when non-zero.\n");
return help;
}
int
cm_opts::set_var(const std::string &key, const std::string &val)
mdc_opts::set_var(const std::string &key, const std::string &val)
{
if (key.length() == 0)
return BRLCAD_ERROR;
if (key.empty())
return BRLCAD_ERROR;
const char *cstr[2];
cstr[0] = val.c_str();
cstr[1] = NULL;
const char *value[2] = {val.c_str(), NULL};
if (key == "feature_size") {
fastf_t parsed = 0.0;
if ((!val.empty() &&
bu_opt_fastf_t(NULL, 1, value, &parsed) < 0) ||
parsed < 0.0)
return BRLCAD_ERROR;
feature_size = parsed;
return BRLCAD_OK;
}
if (key == std::string("max_cycle_time")) {
if (!val.length()) {
max_cycle_time = 0;
return BRLCAD_OK;
}
if (bu_opt_int(NULL, 1, (const char **)cstr, (void *)&max_cycle_time) < 0)
return BRLCAD_ERROR;
}
if (key == "max_rays" || key == "minimum_free_mem") {
long parsed = 0;
if ((!val.empty() && bu_opt_long(NULL, 1, value, &parsed) < 0) ||
parsed < 0)
return BRLCAD_ERROR;
if (key == "max_rays")
max_rays = parsed;
else
minimum_free_mem = parsed;
return BRLCAD_OK;
}
if (key == std::string("max_time")) {
if (!val.length()) {
max_time = 0;
return BRLCAD_OK;
}
if (bu_opt_int(NULL, 1, (const char **)cstr, (void *)&max_time) < 0)
return BRLCAD_ERROR;
}
int parsed = 0;
if (!val.empty() && bu_opt_int(NULL, 1, value, &parsed) < 0)
return BRLCAD_ERROR;
if (key == "min_depth") {
if (parsed < 1 || parsed > ANALYZE_MDC_MAX_DEPTH)
return BRLCAD_ERROR;
min_depth = parsed;
return BRLCAD_OK;
}
if (key == "max_depth") {
if (parsed < 1 || parsed > ANALYZE_MDC_MAX_DEPTH)
return BRLCAD_ERROR;
max_depth = parsed;
return BRLCAD_OK;
}
if (key == "max_time") {
if (parsed < 0)
return BRLCAD_ERROR;
max_time = parsed;
return BRLCAD_OK;
}
if (key == "verbosity") {
if (parsed < 0)
return BRLCAD_ERROR;
verbosity = parsed;
return BRLCAD_OK;
}
// If it's not a CM setting directly, it may be for sampling
return sample_opts::set_var(key, val);
return BRLCAD_ERROR;
}
void
cm_opts::sync(method_options_t &o)
mdc_opts::sync(method_options_t &options)
{
std::map<std::string, std::map<std::string,std::string>>::iterator o_it;
o_it = o.options_map.find(std::string("CM"));
if (o_it == o.options_map.end())
invalid_option.clear();
auto method = options.options_map.find("MDC");
if (method == options.options_map.end())
return;
std::map<std::string,std::string>::iterator m_it;
for (m_it = o_it->second.begin(); m_it != o_it->second.end(); m_it++) {
set_var(m_it->first, m_it->second);
for (const auto &option : method->second) {
if (set_var(option.first, option.second) != BRLCAD_OK) {
invalid_option = option.first + "=" + option.second;
return;
}
}
}
void
cm_opts::sync(sample_opts &opts)
{
sample_opts::sync(opts);
}
std::string
nmg_opts::about_method()
{
@@ -568,6 +591,9 @@ spsr_opts::print_options_help()
h.append(" mesh, higher values bias towards interpolation\n");
h.append(" accuracy. (Default is 2.0)\n");
h.append("max_time - Maximum overall run time for object conversion\n");
h.append("refinement_passes- Maximum adaptive source-refinement passes. (Default is ");
h.append(std::to_string(BG_3D_SPSR_DEFAULT_REFINEMENT_PASSES));
h.append(")\n");
h.append("samples_per_node - How many samples should go into a cell before it is\n");
h.append(" refined. (Default is 1.5)\n");
return h;
@@ -576,48 +602,53 @@ spsr_opts::print_options_help()
int
spsr_opts::set_var(const std::string &key, const std::string &val)
{
if (key.length() == 0)
if (key.empty())
return BRLCAD_ERROR;
const char *cstr[2];
cstr[0] = val.c_str();
cstr[1] = NULL;
if (key == std::string("depth")) {
if (!val.length()) {
s_opts.depth = 0;
return BRLCAD_OK;
}
if (bu_opt_int(NULL, 1, (const char **)cstr, (void *)&s_opts.depth) < 0)
const char *value[2] = {val.c_str(), NULL};
if (key == "depth") {
int parsed = BG_3D_SPSR_DEFAULT_DEPTH;
if ((!val.empty() && bu_opt_int(NULL, 1, value, &parsed) < 0) ||
parsed < 1)
return BRLCAD_ERROR;
s_opts.depth = parsed;
return BRLCAD_OK;
}
if (key == std::string("interpolate")) {
if (!val.length()) {
s_opts.point_weight = 0.0;
return BRLCAD_OK;
}
if (bu_opt_fastf_t(NULL, 1, (const char **)cstr, (void *)&s_opts.point_weight) < 0)
if (key == "interpolate") {
fastf_t parsed = BG_3D_SPSR_DEFAULT_POINT_WEIGHT;
if ((!val.empty() &&
bu_opt_fastf_t(NULL, 1, value, &parsed) < 0) ||
parsed < 0.0)
return BRLCAD_ERROR;
s_opts.point_weight = parsed;
return BRLCAD_OK;
}
if (key == std::string("samples_per_node")) {
if (!val.length()) {
s_opts.samples_per_node = 0.0;
return BRLCAD_OK;
}
if (bu_opt_fastf_t(NULL, 1, (const char **)cstr, (void *)&s_opts.samples_per_node) < 0)
if (key == "samples_per_node") {
fastf_t parsed = BG_3D_SPSR_DEFAULT_SAMPLES_PER_NODE;
if ((!val.empty() &&
bu_opt_fastf_t(NULL, 1, value, &parsed) < 0) ||
parsed <= 0.0)
return BRLCAD_ERROR;
s_opts.samples_per_node = parsed;
return BRLCAD_OK;
}
if (key == "refinement_passes") {
int parsed = BG_3D_SPSR_DEFAULT_REFINEMENT_PASSES;
if ((!val.empty() && bu_opt_int(NULL, 1, value, &parsed) < 0) ||
parsed < 0)
return BRLCAD_ERROR;
refinement_passes = parsed;
return BRLCAD_OK;
}
if (key == "max_time") {
int parsed = 600;
if ((!val.empty() && bu_opt_int(NULL, 1, value, &parsed) < 0) ||
parsed < 0)
return BRLCAD_ERROR;
max_time = parsed;
return BRLCAD_OK;
}
if (key == std::string("max_time")) {
if (!val.length()) {
max_time = 0;
return BRLCAD_OK;
}
if (bu_opt_int(NULL, 1, (const char **)cstr, (void *)&max_time) < 0)
return BRLCAD_ERROR;
}
// If it's not a SPSR setting directly, it may be for sampling
return sample_opts::set_var(key, val);
}
+35
View File
@@ -0,0 +1,35 @@
brlcad_addexec(facetize_worker_protocol_test "worker.cpp;../worker.cpp" "libbu;Threads::Threads" TEST)
brlcad_add_test(NAME facetize_worker_protocol COMMAND facetize_worker_protocol_test)
brlcad_addexec(facetize_validation_test "validation.cpp;../validation.cpp" "libwdb;librt;libbu" TEST)
brlcad_add_test(NAME facetize_validation COMMAND facetize_validation_test)
brlcad_addexec(facetize_recovery_test recovery.cpp "libged;libwdb;librt;libbu" TEST_USESDATA)
if(TARGET facetize_recovery_test)
add_dependencies(facetize_recovery_test ged_plugins facetize_process)
set(FACETIZE_RECOVERY_TMP "${CMAKE_CURRENT_BINARY_DIR}/facetize_recovery_tmp")
brlcad_add_test(
NAME facetize_recovery
COMMAND facetize_recovery_test "${FACETIZE_RECOVERY_TMP}"
)
set_tests_properties(
facetize_recovery
PROPERTIES
ENVIRONMENT "BU_DIR_CACHE=${FACETIZE_RECOVERY_TMP}/cache"
TIMEOUT 120
)
endif(TARGET facetize_recovery_test)
cmakefiles(
CMakeLists.txt
recovery.cpp
validation.cpp
worker.cpp
)
# Local Variables:
# tab-width: 8
# mode: cmake
# indent-tabs-mode: t
# End:
# ex: shiftwidth=2 tabstop=8
+359
View File
@@ -0,0 +1,359 @@
/* F A C E T I Z E _ R E C O V E R Y . C P P
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. The name of the author may not be used to endorse or promote
* products derived from this software without specific prior written
* permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
* GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
* IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
* IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/** @file facetize/tests/recovery.cpp
*
* End-to-end fault injection tests for facetize worker write recovery.
*/
#include "common.h"
#include <filesystem>
#include <string>
#include <vector>
#include <string.h>
#include "bu/app.h"
#include "bu/env.h"
#include "bu/file.h"
#include "bu/log.h"
#include "bu/vls.h"
#include "raytrace.h"
#include "wdb.h"
#include "ged.h"
static const char FAULT_ENV[] = "LIBGED_FACETIZE_TEST_FAULT";
static const char FAULT_FILE_ENV[] = "LIBGED_FACETIZE_TEST_FAULT_FILE";
static const char TEST_OBJECT[] = "one.s";
static const char TEST_COMBINATION[] = "model.c";
static const char TEST_OUTPUT[] = "model.bot";
struct recovery_case {
const char *name;
const char *stage;
bool expect_success;
bool expect_replay;
bool expect_resubmit;
};
static int
capture_log(void *data, void *message)
{
if (data && message)
static_cast<std::string *>(data)->append(static_cast<const char *>(message));
return 0;
}
static int
create_input(const char *gfile)
{
(void)bu_file_delete(gfile);
struct db_i *dbip = db_create(gfile, BRLCAD_DB_FORMAT_LATEST);
if (!dbip)
return BRLCAD_ERROR;
struct rt_wdb *wdbp = wdb_dbopen(dbip, RT_WDB_TYPE_DB_DEFAULT);
if (!wdbp) {
db_close(dbip);
return BRLCAD_ERROR;
}
point_t centers[3] = {
{0.0, 0.0, 0.0},
{30.0, 0.0, 0.0},
{60.0, 0.0, 0.0}
};
const char *names[3] = {TEST_OBJECT, "two.s", "three.s"};
int ret = BRLCAD_OK;
for (size_t i = 0; i < 3; i++) {
if (mk_sph(wdbp, names[i], centers[i], 10.0) < 0) {
ret = BRLCAD_ERROR;
break;
}
}
if (ret == BRLCAD_OK) {
struct bu_list members;
BU_LIST_INIT(&members);
for (const char *name : names)
(void)mk_addmember(name, &members, NULL, WMOP_UNION);
if (mk_comb(wdbp, TEST_COMBINATION, &members, 0, NULL, NULL, NULL,
0, 0, 0, 0, 0, 0, 0) < 0)
ret = BRLCAD_ERROR;
}
wdb_close(wdbp);
return ret;
}
static int
run_facetize(const char *gfile, int jobs, std::string &output)
{
struct ged *gedp = ged_open("db", gfile, 1);
if (!gedp)
return BRLCAD_ERROR;
std::string log_output;
struct bu_hook_list saved_hooks = BU_HOOK_LIST_INIT_ZERO;
bu_log_hook_save_all(&saved_hooks);
bu_log_hook_delete_all();
bu_log_add_hook(capture_log, &log_output);
std::string job_count = std::to_string(jobs);
const char *argv[] = {
"facetize", "-v", "--jobs", job_count.c_str(), "--methods", "NMG",
TEST_COMBINATION, TEST_OUTPUT, NULL
};
int ret = ged_exec(gedp, 8, argv);
output = bu_vls_cstr(gedp->ged_result_str);
ged_close(gedp);
bu_log_hook_delete_all();
bu_log_hook_restore_all(&saved_hooks);
bu_hook_delete_all(&saved_hooks);
output.append(log_output);
return ret;
}
static bool
object_exists(const char *gfile, const char *object_name)
{
struct db_i *dbip = db_open(gfile, DB_OPEN_READONLY);
if (!dbip)
return false;
bool exists = db_dirbuild(dbip) >= 0 &&
db_lookup(dbip, object_name, LOOKUP_QUIET) != RT_DIR_NULL;
db_close(dbip);
return exists;
}
static bool
object_external(const char *gfile, const char *object_name,
std::vector<unsigned char> &bytes)
{
bytes.clear();
struct db_i *dbip = db_open(gfile, DB_OPEN_READONLY);
if (!dbip)
return false;
if (db_dirbuild(dbip) < 0) {
db_close(dbip);
return false;
}
struct directory *dp = db_lookup(dbip, object_name, LOOKUP_QUIET);
struct bu_external external;
if (!dp || db_get_external(&external, dp, dbip) < 0) {
db_close(dbip);
return false;
}
const unsigned char *data =
static_cast<const unsigned char *>(external.ext_buf);
bytes.assign(data, data + external.ext_nbytes);
bu_free_external(&external);
db_close(dbip);
return true;
}
static bool
outputs_match(const char *control_file, const char *test_file)
{
std::vector<unsigned char> control;
std::vector<unsigned char> test;
return object_external(control_file, TEST_OUTPUT, control) &&
object_external(test_file, TEST_OUTPUT, test) && control == test;
}
static bool
has_temporary_write_file(const std::filesystem::path &cache_dir)
{
std::error_code error;
std::filesystem::recursive_directory_iterator entry(cache_dir, error);
std::filesystem::recursive_directory_iterator end;
while (!error && entry != end) {
if (entry->is_regular_file(error)) {
std::string name = entry->path().filename().string();
if (name.find(".facetize_worker_") != std::string::npos ||
entry->path().extension() == ".bak" ||
name.find(".io_probe") != std::string::npos)
return true;
}
entry.increment(error);
}
return false;
}
static bool
reset_cache(const std::filesystem::path &cache_dir)
{
std::error_code error;
std::filesystem::remove_all(cache_dir, error);
if (error)
return false;
return std::filesystem::create_directories(cache_dir, error) && !error;
}
static int
run_recovery_case(const recovery_case &test_case,
const std::filesystem::path &test_dir,
const std::filesystem::path &cache_dir, const char *control_file)
{
std::filesystem::path gfile = test_dir /
(std::string(test_case.name) + ".g");
std::filesystem::path claim_file = test_dir /
(std::string(test_case.name) + ".fault");
(void)bu_file_delete(claim_file.string().c_str());
if (create_input(gfile.string().c_str()) != BRLCAD_OK) {
bu_log("[facetize_recovery] %s: unable to create input\n",
test_case.name);
return BRLCAD_ERROR;
}
std::string selector = std::string(test_case.stage) + ":" + TEST_OBJECT;
(void)bu_setenv(FAULT_ENV, selector.c_str(), 1);
(void)bu_setenv(FAULT_FILE_ENV, claim_file.string().c_str(), 1);
std::string output;
int ret = run_facetize(gfile.string().c_str(), 2, output);
(void)bu_setenv(FAULT_ENV, "", 1);
(void)bu_setenv(FAULT_FILE_ENV, "", 1);
bool passed = true;
if (!bu_file_exists(claim_file.string().c_str(), NULL)) {
bu_log("[facetize_recovery] %s: fault did not trigger\n",
test_case.name);
passed = false;
}
if ((ret == BRLCAD_OK) != test_case.expect_success) {
bu_log("[facetize_recovery] %s: unexpected command result %d\n%s\n",
test_case.name, ret, output.c_str());
passed = false;
}
if (test_case.expect_success) {
if (!outputs_match(control_file, gfile.string().c_str())) {
bu_log("[facetize_recovery] %s: result differs from serial control\n",
test_case.name);
passed = false;
}
} else if (object_exists(gfile.string().c_str(), TEST_OUTPUT)) {
bu_log("[facetize_recovery] %s: failed command left an output object\n",
test_case.name);
passed = false;
}
bool replay_reported = output.find("retrying tessellation") !=
std::string::npos;
if (replay_reported != test_case.expect_replay) {
bu_log("[facetize_recovery] %s: unexpected replay reporting\n%s\n",
test_case.name, output.c_str());
passed = false;
}
bool resubmit_reported = output.find("resubmitted staged tessellation") !=
std::string::npos;
if (resubmit_reported != test_case.expect_resubmit) {
bu_log("[facetize_recovery] %s: unexpected resubmission reporting\n%s\n",
test_case.name, output.c_str());
passed = false;
}
if (has_temporary_write_file(cache_dir)) {
bu_log("[facetize_recovery] %s: temporary write file was not cleaned\n",
test_case.name);
passed = false;
}
bu_log("[facetize_recovery] %s: %s\n", test_case.name,
passed ? "PASS" : "FAIL");
return passed ? BRLCAD_OK : BRLCAD_ERROR;
}
int
main(int argc, const char **argv)
{
bu_setprogname(argv[0]);
if (argc != 2) {
bu_log("Usage: %s test-directory\n", argv[0]);
return 1;
}
std::filesystem::path test_dir(argv[1]);
std::filesystem::path cache_dir = test_dir / "cache";
std::error_code error;
std::filesystem::remove_all(test_dir, error);
if (error) {
bu_log("[facetize_recovery] unable to clear test directory\n");
return 1;
}
if (!std::filesystem::create_directories(cache_dir, error) || error) {
bu_log("[facetize_recovery] unable to create test directory\n");
return 1;
}
std::filesystem::path control_file = test_dir / "control.g";
std::string control_output;
if (create_input(control_file.string().c_str()) != BRLCAD_OK ||
run_facetize(control_file.string().c_str(), 1, control_output) !=
BRLCAD_OK) {
bu_log("[facetize_recovery] unable to create serial control\n%s\n",
control_output.c_str());
return 1;
}
const recovery_case cases[] = {
{"tess_before_ready", "tess-before-ready", false, false, false},
{"stage_before_done", "stage-before-done", false, false, false},
{"writer_before_proceed", "writer-before-proceed", true, false, true},
{"writer_before_done", "writer-before-done", true, true, false}
};
int ret = 0;
for (const recovery_case &test_case : cases) {
if (!reset_cache(cache_dir)) {
bu_log("[facetize_recovery] %s: unable to reset cache directory\n",
test_case.name);
ret = 1;
continue;
}
if (run_recovery_case(test_case, test_dir, cache_dir,
control_file.string().c_str()) != BRLCAD_OK)
ret = 1;
}
if (!ret)
std::filesystem::remove_all(test_dir, error);
return ret;
}
// Local Variables:
// tab-width: 8
// mode: C++
// c-basic-offset: 4
// indent-tabs-mode: t
// c-file-style: "stroustrup"
// End:
// ex: shiftwidth=4 tabstop=8
+154
View File
@@ -0,0 +1,154 @@
/* F A C E T I Z E _ V A L I D A T I O N . C P P
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. The name of the author may not be used to endorse or promote
* products derived from this software without specific prior written
* permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
* GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
* IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
* IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/** @file facetize/tests/validation.cpp
*
* Exercise finite and unbounded CSG reference sampling.
*/
#include "common.h"
#include <cmath>
#include <cstdio>
#include "bu/app.h"
#include "bu/log.h"
#include "raytrace.h"
#include "wdb.h"
#include "../validation.h"
static int failures = 0;
static int
count_log(void *data, void *UNUSED(message))
{
if (data)
(*static_cast<int *>(data))++;
return 0;
}
static void
expect(bool condition, const char *message)
{
if (condition)
return;
std::fprintf(stderr, "FAIL: %s\n", message);
failures++;
}
int
main(int UNUSED(argc), const char **argv)
{
bu_setprogname(argv[0]);
struct db_i *dbip = db_create_inmem();
expect(dbip != NULL, "create in-memory database");
if (!dbip)
return 1;
struct rt_wdb *wdbp = wdb_dbopen(dbip, RT_WDB_TYPE_DB_INMEM);
expect(wdbp != NULL, "open in-memory database for writing");
if (!wdbp) {
db_close(dbip);
return 1;
}
point_t center = VINIT_ZERO;
vect_t normal = {1.0, 0.0, 0.0};
expect(mk_sph(wdbp, "sphere.s", center, 10.0) == 0,
"create bounded test solid");
expect(mk_half(wdbp, "half.s", normal, 0.0) == 0,
"create unbounded test solid");
struct bu_list members;
BU_LIST_INIT(&members);
expect(mk_addmember("sphere.s", &members, NULL, WMOP_UNION) != NULL,
"add bounded member");
expect(mk_addmember("half.s", &members, NULL, WMOP_INTERSECT) != NULL,
"add halfspace clipping member");
expect(mk_comb(wdbp, "clipped.r", &members, 1, NULL, NULL, NULL,
0, 0, 0, 0, 0, 0, 0) == 0, "create clipped test region");
struct bu_list empty_members;
BU_LIST_INIT(&empty_members);
expect(mk_comb(wdbp, "empty.c", &empty_members, 0, NULL, NULL, NULL,
0, 0, 0, 0, 0, 0, 0) == 0, "create empty test combination");
double surface_area = -1.0;
double volume = -1.0;
int log_count = 0;
struct bu_hook_list saved_hooks = BU_HOOK_LIST_INIT_ZERO;
bu_log_hook_save_all(&saved_hooks);
bu_log_hook_delete_all();
bu_log_add_hook(count_log, &log_count);
long crossings = facetize_csg_metrics(dbip, "half.s", &surface_area,
&volume);
expect(crossings < 0,
"unbounded halfspace is not assigned Crofton metrics");
crossings = facetize_csg_metrics(dbip, "empty.c", &surface_area,
&volume);
int bound_log_count = log_count;
bu_log("facetize validation hook restoration probe\n");
bu_log_hook_delete_all();
bu_log_hook_restore_all(&saved_hooks);
bu_hook_delete_all(&saved_hooks);
expect(crossings < 0,
"empty combination is not assigned Crofton metrics");
expect(bound_log_count == 0,
"expected bounding failure diagnostics are suppressed");
expect(log_count == 1,
"bounding failure log suppression restores existing hooks");
crossings = facetize_csg_metrics(dbip, "clipped.r", &surface_area,
&volume);
expect(crossings > 0,
"bounded geometry clipped by a halfspace is sampled");
expect(std::isfinite(surface_area) && surface_area > 0.0,
"clipped geometry has finite positive surface area");
expect(std::isfinite(volume) && volume > 0.0,
"clipped geometry has finite positive volume");
db_close(dbip);
return failures ? 1 : 0;
}
// Local Variables:
// tab-width: 8
// mode: C++
// c-basic-offset: 4
// indent-tabs-mode: t
// c-file-style: "stroustrup"
// End:
// ex: shiftwidth=4 tabstop=8
+601
View File
@@ -0,0 +1,601 @@
/* F A C E T I Z E _ W O R K E R . C P P
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. The name of the author may not be used to endorse or promote
* products derived from this software without specific prior written
* permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
* GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
* IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
* IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/** @file facetize/tests/worker.cpp
*
* Exercise facetize worker protocol framing and state parsing.
*/
#include "common.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <string>
#include <vector>
#include "bu/app.h"
#include "../worker.h"
static int failures = 0;
static const double METRIC_COMPARISON_TOLERANCE = 1.0e-12;
static const size_t PROTOCOL_OVERSIZED_FIELD_LENGTH = 1024u * 1024u + 1u;
static void
expect(bool condition, const char *message)
{
if (condition)
return;
std::fprintf(stderr, "FAIL: %s\n", message);
failures++;
}
static FILE *
stream_with_contents(const std::string &contents)
{
FILE *stream = tmpfile();
if (!stream)
return NULL;
if (fwrite(contents.data(), 1, contents.size(), stream) != contents.size() ||
fflush(stream) != 0 || fseek(stream, 0, SEEK_SET) != 0) {
fclose(stream);
return NULL;
}
return stream;
}
static std::string
read_stream(FILE *stream)
{
std::string contents;
char buffer[64];
if (fflush(stream) != 0 || fseek(stream, 0, SEEK_SET) != 0)
return contents;
size_t bytes_read = 0;
while ((bytes_read = fread(buffer, 1, sizeof(buffer), stream)) > 0)
contents.append(buffer, bytes_read);
return contents;
}
static void
test_request_round_trip()
{
FILE *requests = tmpfile();
expect(requests != NULL, "create request stream");
if (!requests)
return;
FacetizeWorkerRequest request;
request.operation = FacetizeWorkerOperation::TessellatePrimitive;
request.input_names.push_back("assembly/part\nwith spaces");
request.primitive.methods.push_back("NMG");
request.primitive.method_options.push_back("NMG max_time=30\ntol=0.01");
request.primitive.cache_directory = "cache path\nwith spaces";
request.primitive.point_limit = 12345;
FacetizeWorkerClient client;
client.reset(requests);
expect(client.send_request(request), "client sends a typed primitive request");
FacetizeWorkerRequest region_request;
region_request.operation = FacetizeWorkerOperation::EvaluateRegion;
region_request.input_names.push_back("assembly.r");
region_request.region.no_empty = true;
region_request.region.no_fixup = true;
region_request.region.tolerate_failures = true;
expect(client.send_request(region_request), "client sends a typed region request");
FacetizeWorkerRequest validation_request;
validation_request.operation = FacetizeWorkerOperation::ValidateCsg;
validation_request.input_names.push_back("assembly.r");
expect(client.send_request(validation_request),
"client sends a typed validation request");
FacetizeWorkerRequest nmg_request;
nmg_request.operation = FacetizeWorkerOperation::NmgBooleanToNmg;
nmg_request.input_names.push_back("first.r");
nmg_request.input_names.push_back("second.r");
nmg_request.output_name = "combined.nmg";
expect(client.send_request(nmg_request),
"client sends an NMG Boolean request");
expect(fflush(requests) == 0 && fseek(requests, 0, SEEK_SET) == 0,
"rewind request stream");
FacetizeWorkerServer server(requests, NULL);
FacetizeWorkerRequest received;
expect(server.receive_request(received) == FacetizeWorkerReadResult::Request,
"server accepts a complete request");
expect(received.operation == request.operation,
"request framing preserves the operation");
expect(received.input_names == request.input_names,
"request framing preserves object name bytes");
expect(received.primitive.methods == request.primitive.methods &&
received.primitive.method_options == request.primitive.method_options,
"request framing preserves tessellation settings");
expect(received.primitive.cache_directory ==
request.primitive.cache_directory &&
received.primitive.point_limit == request.primitive.point_limit,
"request framing preserves cache and point settings");
expect(server.receive_request(received) == FacetizeWorkerReadResult::Request,
"server accepts a region request");
expect(received.operation == FacetizeWorkerOperation::EvaluateRegion &&
received.input_names == region_request.input_names &&
received.region.no_empty && received.region.no_fixup &&
received.region.tolerate_failures,
"request framing preserves region settings");
expect(server.receive_request(received) == FacetizeWorkerReadResult::Request,
"server accepts a validation request");
expect(received.operation == FacetizeWorkerOperation::ValidateCsg &&
received.input_names == validation_request.input_names &&
received.primitive.empty() && received.region.empty(),
"validation request has no unrelated settings");
expect(server.receive_request(received) == FacetizeWorkerReadResult::Request,
"server accepts an NMG Boolean request");
expect(received.operation == FacetizeWorkerOperation::NmgBooleanToNmg &&
received.input_names == nmg_request.input_names &&
received.output_name == nmg_request.output_name,
"request framing preserves NMG Boolean inputs and output");
expect(server.receive_request(received) == FacetizeWorkerReadResult::End,
"clean request EOF ends the worker loop");
fclose(requests);
client.reset(NULL);
expect(!client.send_request(request),
"client rejects a missing request stream");
FILE *invalid_stream = tmpfile();
expect(invalid_stream != NULL, "create invalid request stream");
if (invalid_stream) {
client.reset(invalid_stream);
FacetizeWorkerRequest invalid;
expect(!client.send_request(invalid),
"client rejects a request without an input");
invalid.input_names.push_back("");
expect(!client.send_request(invalid),
"client rejects an empty input name");
invalid.input_names.front() = "part";
invalid.primitive.point_limit = -1;
expect(!client.send_request(invalid),
"client rejects a negative point limit");
invalid.primitive.point_limit = 0;
invalid.region.no_empty = true;
expect(!client.send_request(invalid),
"client rejects region settings on a primitive request");
invalid.operation = FacetizeWorkerOperation::ValidateCsg;
invalid.region = FacetizeRegionSettings();
invalid.primitive.methods.push_back("NMG");
expect(!client.send_request(invalid),
"client rejects tessellation settings on a validation request");
invalid.operation = FacetizeWorkerOperation::TessellatePrimitive;
invalid.primitive.methods.clear();
invalid.input_names.front().assign(PROTOCOL_OVERSIZED_FIELD_LENGTH, 'x');
expect(!client.send_request(invalid),
"client rejects an oversized field");
expect(ftell(invalid_stream) == 0,
"invalid requests do not partially modify the request stream");
invalid.input_names.front() = "part";
invalid.input_names.push_back("other");
expect(!client.send_request(invalid),
"client rejects multiple primitive inputs");
fclose(invalid_stream);
}
}
static void
test_malformed_requests()
{
const char *invalid_requests[] = {
"NOT_A_REQUEST 3 1 1 0 0 0 0 0 0\n",
"FACETIZE_REQUEST 2 1 1 0 0 0 0 0 0\n",
"FACETIZE_REQUEST 3 99 1 0 0 0 0 0 0\n",
"FACETIZE_REQUEST 3 1 0 0 0 0 0 0 0\n",
"FACETIZE_REQUEST 3 1 1 0 0 0 2 0 0\n",
"FACETIZE_REQUEST 3 1 1 0 0 0 0 0 0 trailing\n",
"FACETIZE_REQUEST 3 1 1 0 0 0 0 0 0\n0\n\n0\n\n5\npart\n",
"FACETIZE_REQUEST 3 1 1 0 0 0 0 0 0\n0\n\n0\n\n4\npart!"
};
for (const char *invalid_request : invalid_requests) {
FILE *stream = stream_with_contents(invalid_request);
expect(stream != NULL, "create malformed request stream");
if (!stream)
continue;
FacetizeWorkerServer server(stream, NULL);
FacetizeWorkerRequest decoded_request;
expect(server.receive_request(decoded_request) == FacetizeWorkerReadResult::Error,
"server rejects malformed request framing");
expect(decoded_request.input_names.empty(),
"malformed request does not expose partial inputs");
fclose(stream);
}
const size_t unterminated_header_size = 128;
std::string long_header(unterminated_header_size, 'x');
FILE *stream = stream_with_contents(long_header);
expect(stream != NULL, "create overlong header stream");
if (stream) {
FacetizeWorkerServer server(stream, NULL);
FacetizeWorkerRequest request;
expect(server.receive_request(request) == FacetizeWorkerReadResult::Error,
"server rejects a header without a terminator");
fclose(stream);
}
}
static void
test_commit_round_trip()
{
FILE *requests = tmpfile();
expect(requests != NULL, "create commit stream");
if (!requests)
return;
const char result_file[] = "cache path/worker\nresult.g";
const char object_name[] = "assembly/part with spaces";
const size_t payload_size = 1234567;
FacetizeWorkerClient client;
client.reset(requests);
expect(client.send_commit(result_file, object_name, payload_size),
"client sends a framed commit request");
expect(fflush(requests) == 0 && fseek(requests, 0, SEEK_SET) == 0,
"rewind commit stream");
FacetizeWorkerServer server(requests, NULL);
FacetizeCommitRequest request;
expect(server.receive_commit(request) == FacetizeWorkerReadResult::Request,
"server accepts a complete commit request");
expect(request.result_file == result_file,
"commit framing preserves result path bytes");
expect(request.object_name == object_name,
"commit framing preserves object name bytes");
expect(request.payload_size == payload_size,
"commit framing preserves the payload size");
expect(server.receive_commit(request) == FacetizeWorkerReadResult::End,
"clean commit EOF ends the writer loop");
fclose(requests);
client.reset(NULL);
expect(!client.send_commit("result.g", "part", 1),
"client rejects a commit without a request stream");
FILE *invalid_requests = tmpfile();
expect(invalid_requests != NULL, "create invalid commit stream");
if (invalid_requests) {
client.reset(invalid_requests);
expect(!client.send_commit(NULL, "part", 1),
"client rejects a null result path");
expect(!client.send_commit("", "part", 1),
"client rejects an empty result path");
expect(!client.send_commit("result.g", NULL, 1),
"client rejects a null object name");
expect(!client.send_commit("result.g", "", 1),
"client rejects an empty object name");
fclose(invalid_requests);
}
const char *malformed_commits[] = {
"NOT_A_COMMIT 8 4 12\nresult.g\npart\n",
"FACETIZE_COMMIT 0 4 12\n\npart\n",
"FACETIZE_COMMIT 8 4 12 trailing\nresult.g\npart\n",
"FACETIZE_COMMIT 9 4 12\nresult.g\npart\n",
"FACETIZE_COMMIT 8 5 12\nresult.g\npart\n",
"FACETIZE_COMMIT 8 4 12\nresult.g!part\n"
};
for (const char *commit : malformed_commits) {
FILE *malformed = stream_with_contents(commit);
expect(malformed != NULL, "create malformed commit stream");
if (!malformed)
continue;
FacetizeWorkerServer malformed_server(malformed, NULL);
expect(malformed_server.receive_commit(request) ==
FacetizeWorkerReadResult::Error,
"server rejects malformed commit framing");
expect(request.result_file.empty() && request.object_name.empty(),
"malformed commit does not expose partial fields");
fclose(malformed);
}
}
static void
test_write_handshake()
{
FILE *requests = tmpfile();
FILE *responses = tmpfile();
expect(requests != NULL && responses != NULL, "create handshake streams");
if (!requests || !responses) {
if (requests) fclose(requests);
if (responses) fclose(responses);
return;
}
FacetizeWorkerClient client;
client.reset(requests);
expect(client.send_write_proceed(), "client sends write acknowledgement");
expect(fflush(requests) == 0 && fseek(requests, 0, SEEK_SET) == 0,
"rewind write acknowledgement");
FacetizeWorkerServer server(requests, responses);
expect(server.receive_write_proceed(), "server accepts exact write acknowledgement");
const size_t announced_payload_size = 987654;
const size_t announced_resident_size = 456789;
expect(server.send_write_ready(announced_payload_size,
announced_resident_size), "server sends write size");
const int tolerated_failures = 3;
expect(server.send_region_write_ready(announced_payload_size,
announced_resident_size, tolerated_failures),
"server sends region write size and tolerated-failure count");
expect(server.send_write_started(), "server acknowledges write start");
expect(server.send_write_result(BRLCAD_OK, announced_resident_size),
"server sends write completion");
std::string output = read_stream(responses);
FacetizeWorkerStatus status;
std::vector<std::string> diagnostics;
const size_t fragment_size = 3;
for (size_t offset = 0; offset < output.size();) {
const size_t chunk_size = std::min(fragment_size, output.size() - offset);
client.consume_output(output.data() + offset, chunk_size, status, diagnostics);
offset += chunk_size;
}
expect(status.write_ready, "client recognizes fragmented write-ready message");
expect(status.payload_size == announced_payload_size,
"client preserves announced write size");
expect(status.resident_size == announced_resident_size,
"client preserves worker resident memory");
expect(status.tolerated_failures == tolerated_failures,
"client preserves the region's tolerated-failure count");
expect(status.write_started,
"client recognizes fragmented write-start acknowledgement");
expect(status.write_done, "client recognizes fragmented write completion");
expect(status.result_received && status.result == BRLCAD_OK,
"write completion result is retained");
expect(diagnostics.empty(), "valid protocol output emits no diagnostics");
fclose(requests);
fclose(responses);
client.reset(NULL);
FILE *failure_response = tmpfile();
expect(failure_response != NULL, "create tessellation result stream");
if (failure_response) {
FacetizeWorkerServer failure_server(NULL, failure_response);
expect(failure_server.send_tessellation_result(BRLCAD_ERROR,
announced_resident_size),
"server sends pre-write tessellation failure");
std::string failure_output = read_stream(failure_response);
FacetizeWorkerStatus failure_status;
diagnostics.clear();
client.consume_output(failure_output.data(), failure_output.size(),
failure_status, diagnostics);
expect(failure_status.result_received &&
failure_status.result == BRLCAD_ERROR && !failure_status.write_done,
"client distinguishes pre-write failure from write completion");
expect(failure_status.resident_size == announced_resident_size,
"pre-write result reports worker resident memory");
expect(diagnostics.empty(), "valid tessellation failure emits no diagnostics");
fclose(failure_response);
}
FILE *bad_ack = stream_with_contents("FACETIZE_WRITE_PROCEED trailing\n");
expect(bad_ack != NULL, "create invalid acknowledgement stream");
if (bad_ack) {
FacetizeWorkerServer bad_server(bad_ack, NULL);
expect(!bad_server.receive_write_proceed(),
"server rejects non-exact write acknowledgement");
fclose(bad_ack);
}
}
static void
test_csg_result()
{
FILE *responses = tmpfile();
expect(responses != NULL, "create CSG result stream");
if (!responses)
return;
const long crossings = 12345;
const double surface_area = 12.25;
const double volume = 42.5;
const size_t resident_size = 456789;
FacetizeWorkerServer server(NULL, responses);
expect(server.send_csg_result(BRLCAD_OK, crossings, surface_area, volume,
resident_size), "server sends CSG validation metrics");
std::string output = read_stream(responses);
FacetizeWorkerClient client;
FacetizeWorkerStatus status;
std::vector<std::string> diagnostics;
const size_t fragment_size = 3;
for (size_t offset = 0; offset < output.size();) {
const size_t chunk_size = std::min(fragment_size, output.size() - offset);
client.consume_output(output.data() + offset, chunk_size, status,
diagnostics);
offset += chunk_size;
}
expect(status.result_received && status.result == BRLCAD_OK,
"client recognizes a fragmented CSG result");
expect(status.csg_crossings == crossings,
"client preserves the CSG crossing count");
expect(std::fabs(status.csg_surface_area - surface_area) <
METRIC_COMPARISON_TOLERANCE,
"client preserves CSG surface area");
expect(std::fabs(status.csg_volume - volume) < METRIC_COMPARISON_TOLERANCE,
"client preserves CSG volume");
expect(status.resident_size == resident_size,
"CSG result reports worker resident memory");
expect(!status.write_ready && !status.write_started && !status.write_done,
"CSG result does not alter writer state");
expect(diagnostics.empty(), "valid CSG result emits no diagnostics");
fclose(responses);
FacetizeWorkerServer null_server(NULL, NULL);
expect(!null_server.send_csg_result(BRLCAD_OK, crossings, surface_area,
volume, resident_size),
"server cannot send CSG metrics without a response stream");
}
static void
test_client_diagnostics_and_reset()
{
FacetizeWorkerClient client;
FacetizeWorkerStatus status;
std::vector<std::string> diagnostics;
const char malformed_output[] =
"ordinary diagnostic\nFACETIZE_RESULT 0 trailing\n"
"FACETIZE_WRITE_STARTED trailing\n"
"FACETIZE_CSG_RESULT 0 10 1.0\n";
client.consume_output(malformed_output, sizeof(malformed_output) - 1,
status, diagnostics);
expect(!status.result_received, "malformed result is not accepted as protocol");
expect(diagnostics.size() == 4, "ordinary and malformed lines remain diagnostics");
diagnostics.clear();
const char partial_frame[] = "FACETIZE_WRITE_RE";
client.consume_output(partial_frame, sizeof(partial_frame) - 1,
status, diagnostics);
client.reset(NULL);
const char post_reset[] = "ADY 42\n";
client.consume_output(post_reset, sizeof(post_reset) - 1,
status, diagnostics);
expect(!status.write_ready, "reset discards a previous channel's partial frame");
expect(diagnostics.size() == 1 && diagnostics[0] == "ADY 42",
"post-reset bytes are reported independently");
FacetizeWorkerServer null_server(NULL, NULL);
FacetizeWorkerRequest request;
FacetizeCommitRequest commit_request;
expect(null_server.receive_request(request) == FacetizeWorkerReadResult::Error,
"server rejects a missing request stream");
expect(null_server.receive_commit(commit_request) ==
FacetizeWorkerReadResult::Error,
"writer rejects a missing request stream");
expect(!null_server.receive_write_proceed(),
"server cannot receive acknowledgement without a stream");
expect(!null_server.send_write_ready(1, 1),
"server cannot announce a write without a response stream");
expect(!null_server.send_write_started(),
"server cannot acknowledge a write without a response stream");
expect(!null_server.send_tessellation_result(BRLCAD_OK, 1),
"server cannot send a result without a response stream");
expect(!null_server.send_write_result(BRLCAD_OK, 1),
"server cannot send write completion without a response stream");
}
static void
test_worker_policy()
{
const size_t mib = (size_t)1024 * 1024;
const size_t gib = (size_t)1024 * mib;
FacetizeWorkerPolicy automatic(0, 20, 16, 3 * gib, 2560 * mib);
expect(automatic.worker_count() == 2,
"automatic policy conservatively permits two workers");
expect(automatic.threads_per_worker() == 8,
"automatic policy divides CPUs between workers");
expect(automatic.can_dispatch(1, 2 * gib, 512 * mib),
"automatic policy admits work while its memory reserve is intact");
expect(!automatic.can_dispatch(1, 1536 * mib, 512 * mib),
"automatic policy preserves system memory headroom");
expect(!automatic.can_dispatch(1, 2 * gib, 1280 * mib),
"observed worker memory increases the next-job reserve");
expect(!automatic.can_dispatch(1, 0, 0),
"missing current memory data pauses additional dispatches");
expect(automatic.can_dispatch(0, 0, 0),
"one job may make progress when current memory data is missing");
FacetizeWorkerPolicy serial(1, 20, 16, 3 * gib, 2560 * mib);
expect(serial.worker_count() == 1 && serial.threads_per_worker() == 16,
"one requested worker retains the full CPU budget");
expect(!serial.can_dispatch(1, 2 * gib, 0),
"serial policy does not admit a second active task");
FacetizeWorkerPolicy requested(8, 3, 12, 3 * gib, 2560 * mib);
expect(requested.worker_count() == 3,
"requested policy is capped by available work");
expect(requested.threads_per_worker() == 4,
"requested policy divides the CPU budget by effective workers");
FacetizeWorkerPolicy unknown_memory(0, 20, 16, 0, 0);
expect(unknown_memory.worker_count() == 1,
"unknown memory availability falls back to one worker");
FacetizeWorkerPolicy low_memory(4, 20, 16, 3 * gib, gib);
expect(low_memory.worker_count() == 1,
"low memory availability falls back to one worker");
}
int
main(int UNUSED(argc), const char **argv)
{
bu_setprogname(argv[0]);
test_request_round_trip();
test_malformed_requests();
test_commit_round_trip();
test_write_handshake();
test_csg_result();
test_client_diagnostics_and_reset();
test_worker_policy();
if (failures)
std::fprintf(stderr, "%d facetize worker protocol test(s) failed\n", failures);
return failures ? 1 : 0;
}
// Local Variables:
// tab-width: 8
// mode: C++
// c-basic-offset: 4
// indent-tabs-mode: t
// c-file-style: "stroustrup"
// End:
// ex: shiftwidth=4 tabstop=8
+88
View File
@@ -0,0 +1,88 @@
/* T R A N S F E R . C P P
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this file; see the file named COPYING for more
* information.
*/
#include "common.h"
#include "raytrace.h"
#include "./transfer.h"
#include "./worker.h"
int
facetize_transfer_staged_object(struct db_i *target_dbip,
const char *result_file, const char *object_name,
int expected_object_type)
{
if (!target_dbip || !result_file || !object_name ||
expected_object_type <= ID_NULL ||
expected_object_type > ID_MAXIMUM)
return BRLCAD_ERROR;
struct db_i *result_dbip = db_open(result_file, DB_OPEN_READONLY);
if (!result_dbip)
return BRLCAD_ERROR;
if (db_dirbuild(result_dbip) < 0) {
db_close(result_dbip);
return BRLCAD_ERROR;
}
struct directory *result_dp = db_lookup(result_dbip,
FACETIZE_WORKER_RESULT_OBJECT, LOOKUP_QUIET);
struct rt_db_internal intern;
RT_DB_INTERNAL_INIT(&intern);
if (!result_dp || result_dp->d_major_type != DB5_MAJORTYPE_BRLCAD ||
result_dp->d_minor_type != expected_object_type ||
rt_db_get_internal(&intern, result_dp, result_dbip, NULL) < 0 ||
intern.idb_type != expected_object_type) {
if (intern.idb_ptr)
rt_db_free_internal(&intern);
db_close(result_dbip);
return BRLCAD_ERROR;
}
db_close(result_dbip);
struct directory *old_dp = db_lookup(target_dbip, object_name,
LOOKUP_QUIET);
if (old_dp && (db_delete(target_dbip, old_dp) != 0 ||
db_dirdelete(target_dbip, old_dp) != 0)) {
rt_db_free_internal(&intern);
return BRLCAD_ERROR;
}
struct directory *target_dp = db_diradd(target_dbip, object_name,
RT_DIR_PHONY_ADDR, 0, RT_DIR_SOLID, (void *)&intern.idb_type);
if (target_dp == RT_DIR_NULL) {
rt_db_free_internal(&intern);
return BRLCAD_ERROR;
}
if (rt_db_put_internal(target_dp, target_dbip, &intern) < 0)
return BRLCAD_ERROR;
return BRLCAD_OK;
}
// Local Variables:
// tab-width: 8
// mode: C++
// c-basic-offset: 4
// indent-tabs-mode: t
// c-file-style: "stroustrup"
// End:
// ex: shiftwidth=4 tabstop=8
+35
View File
@@ -0,0 +1,35 @@
/* T R A N S F E R . H
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this file; see the file named COPYING for more
* information.
*/
#ifndef LIBGED_FACETIZE_TRANSFER_H
#define LIBGED_FACETIZE_TRANSFER_H
#include "rt/db_instance.h"
/**
* Move the worker's fixed-name staged result into @p target_dbip under
* @p object_name. The expected type is checked before the target changes.
*/
int
facetize_transfer_staged_object(struct db_i *target_dbip,
const char *result_file, const char *object_name,
int expected_object_type);
#endif /* LIBGED_FACETIZE_TRANSFER_H */
File diff suppressed because it is too large Load Diff
+30 -70
View File
@@ -25,7 +25,6 @@
#include "common.h"
#include <cmath>
#include <string.h>
#include <iostream>
@@ -35,12 +34,12 @@
#include "bu/path.h"
#include "bu/ptbl.h"
#include "rt/search.h"
#include "rt/calc.h"
#include "rt/db_instance.h"
#include "rt/primitives/bot.h"
#include "wdb.h"
#include "../ged_private.h"
#include "./ged_facetize.h"
#include "./validation.h"
static const size_t FACETIZE_TERMINAL_DETAIL_LIMIT = 10u;
static const char FACETIZE_LOG_EXTENSION[] = ".log";
@@ -284,72 +283,6 @@ _db_uniq_test(struct bu_vls *n, void *data)
return 0;
}
int
_ged_facetize_csg_bbox(struct db_i *dbip, const char *obj_name, point_t rpp_min, point_t rpp_max)
{
if (!dbip || !obj_name || !rpp_min || !rpp_max)
return BRLCAD_ERROR;
struct directory *dp = db_lookup(dbip, obj_name, LOOKUP_QUIET);
if (dp == RT_DIR_NULL)
return BRLCAD_ERROR;
if (rt_bound_internal(dbip, dp, rpp_min, rpp_max) != 0)
return BRLCAD_ERROR;
vect_t d;
VSUB2(d, rpp_max, rpp_min);
if (d[X] <= 0.0 || d[Y] <= 0.0 || d[Z] <= 0.0)
return BRLCAD_ERROR;
for (int i = 0; i < 3; i++) {
if (!std::isfinite(rpp_min[i]) || !std::isfinite(rpp_max[i]))
return BRLCAD_ERROR;
}
return BRLCAD_OK;
}
int
_ged_validate_objs_list(struct _ged_facetize_state *s, int argc, const char *argv[], int newobj_cnt)
{
int i;
struct ged *gedp = s->gedp;
if (s->in_place && newobj_cnt) {
bu_vls_printf(gedp->ged_result_str, "In place conversion specified, but object list includes objects that do not exist:\n");
for (i = argc - newobj_cnt; i < argc; i++) {
bu_vls_printf(gedp->ged_result_str, " %s\n", argv[i]);
}
bu_vls_printf(gedp->ged_result_str, "\nAborting. When performing an in-place facetization, a single pre-existing object must be specified.\n");
return BRLCAD_ERROR;
}
if (!s->in_place) {
if (newobj_cnt < 1) {
bu_vls_printf(gedp->ged_result_str, "all objects listed already exist, aborting. (Need new object name to write out results to.)\n");
return BRLCAD_ERROR;
}
if (newobj_cnt > 1) {
bu_vls_printf(gedp->ged_result_str, "More than one object listed does not exist:\n");
for (i = argc - newobj_cnt; i < argc; i++) {
bu_vls_printf(gedp->ged_result_str, " %s\n", argv[i]);
}
bu_vls_printf(gedp->ged_result_str, "\nAborting. Need to specify exactly one object name that does not exist to hold facetization output.\n");
return BRLCAD_ERROR;
}
if (argc - newobj_cnt == 0) {
bu_vls_printf(gedp->ged_result_str, "No existing objects specified, nothing to facetize. Aborting.\n");
return BRLCAD_ERROR;
}
}
return BRLCAD_OK;
}
int
_ged_facetize_write_bot(struct db_i *dbip, struct rt_bot_internal *bot, const char *name, int verbosity)
{
@@ -364,6 +297,21 @@ _ged_facetize_write_bot(struct db_i *dbip, struct rt_bot_internal *bot, const ch
bu_avs_init_empty(&intern.idb_avs);
(void)bu_avs_add(&intern.idb_avs, "facetized", "1");
/* In-memory databases do not have the file allocator used by
* rt_db_put_internal. Route them through their dedicated writer so
* isolated workers can finish evaluation before announcing a staged
* disk write to the parent. */
if (!dbip->dbi_filename) {
struct rt_wdb *wdbp = wdb_dbopen(dbip, RT_WDB_TYPE_DB_INMEM);
if (!wdbp || wdb_put_internal(wdbp, name, &intern, 1.0) < 0) {
if (verbosity >= 0)
bu_log("Failed to write %s to in-memory database\n", name);
rt_db_free_internal(&intern);
return BRLCAD_ERROR;
}
return BRLCAD_OK;
}
struct directory *dp = db_diradd(dbip, name, RT_DIR_PHONY_ADDR, 0, RT_DIR_SOLID, (void *)&intern.idb_type);
if (dp == RT_DIR_NULL) {
if (verbosity >= 0)
@@ -584,8 +532,20 @@ bot_fixup(struct _ged_facetize_state *s, struct db_i *wdbip, struct directory *b
// Have faces, test with raytracer
struct rt_i *rtip = rt_i_create(wdbip);
rt_gettree(rtip, bname);
rt_prep(rtip);
struct bu_hook_list saved_hooks = BU_HOOK_LIST_INIT_ZERO;
facetize_log_hooks_silence(&saved_hooks);
int prep_status = rt_gettree(rtip, bname);
if (prep_status == 0)
rt_prep(rtip);
facetize_log_hooks_restore(&saved_hooks);
if (prep_status != 0 || !rtip->stats.nsolids || !rtip->stats.nregions) {
facetize_log(s, 2, "\t%s: raytrace preparation failed; retaining original manifold result.\n", bname);
rt_i_destroy(rtip);
rt_db_free_internal(&bot_intern);
return NULL;
}
facetize_log(s, 2, "\t%s: raytrace preparation complete; scanning %zu faces...\n", bname, bot->num_faces);
struct bu_ptbl tfaces = BU_PTBL_INIT_ZERO;
int have_thin_faces = rt_bot_thin_check(&tfaces, bot, rtip, VUNITIZE_TOL, 0);
+158
View File
@@ -0,0 +1,158 @@
/* V A L I D A T I O N . C P P
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*/
#include "common.h"
#include <cmath>
#include "bu/log.h"
#include "rt/calc.h"
#include "raytrace.h"
#include "./ged_facetize.h"
#include "./validation.h"
static int
facetize_discard_log(void *UNUSED(data), void *UNUSED(message))
{
return 0;
}
void
facetize_log_hooks_silence(struct bu_hook_list *saved_hooks)
{
if (!saved_hooks)
return;
bu_log_hook_save_all(saved_hooks);
bu_log_hook_delete_all();
bu_log_add_hook(facetize_discard_log, NULL);
}
void
facetize_log_hooks_restore(struct bu_hook_list *saved_hooks)
{
if (!saved_hooks)
return;
bu_log_hook_delete_all();
bu_log_hook_restore_all(saved_hooks);
bu_hook_delete_all(saved_hooks);
}
static int
facetize_bound_internal(struct db_i *dbip, struct directory *dp,
point_t bounds_min, point_t bounds_max)
{
/* A failed bound is an expected validation outcome for unbounded or
* otherwise unsampleable CSG. rt_bound_internal logs every failure,
* which floods large runs before facetize can report the aggregate. */
struct bu_hook_list saved_hooks = BU_HOOK_LIST_INIT_ZERO;
facetize_log_hooks_silence(&saved_hooks);
int ret = BRLCAD_ERROR;
if (!BU_SETJUMP) {
ret = rt_bound_internal(dbip, dp, bounds_min, bounds_max);
} else {
BU_UNSETJUMP;
facetize_log_hooks_restore(&saved_hooks);
return BRLCAD_ERROR;
}
BU_UNSETJUMP;
facetize_log_hooks_restore(&saved_hooks);
return ret;
}
int
_ged_facetize_csg_bbox(struct db_i *dbip, const char *object_name,
point_t bounds_min, point_t bounds_max)
{
if (!dbip || !object_name || !bounds_min || !bounds_max)
return BRLCAD_ERROR;
struct directory *dp = db_lookup(dbip, object_name, LOOKUP_QUIET);
if (dp == RT_DIR_NULL ||
facetize_bound_internal(dbip, dp, bounds_min, bounds_max) != 0)
return BRLCAD_ERROR;
vect_t dimensions;
VSUB2(dimensions, bounds_max, bounds_min);
if (dimensions[X] <= 0.0 || dimensions[Y] <= 0.0 ||
dimensions[Z] <= 0.0)
return BRLCAD_ERROR;
for (int i = 0; i < 3; i++) {
if (!std::isfinite(bounds_min[i]) || !std::isfinite(bounds_max[i]))
return BRLCAD_ERROR;
}
return BRLCAD_OK;
}
long
facetize_csg_metrics(struct db_i *dbip, const char *object_name,
double *surface_area, double *volume)
{
if (!dbip || !object_name || !object_name[0] || !surface_area || !volume)
return -1L;
*surface_area = -1.0;
*volume = -1.0;
point_t focus_min, focus_max;
if (_ged_facetize_csg_bbox(dbip, object_name, focus_min, focus_max) !=
BRLCAD_OK) {
/* Surface area and volume are not defined for an unbounded CSG result.
* A finite combination clipped by a halfspace still has valid bounds and
* proceeds; a standalone or otherwise unbounded halfspace stops here. */
return -1L;
}
struct rt_i *rtip = rt_i_create(dbip);
if (!rtip)
return -1L;
struct bu_hook_list saved_hooks = BU_HOOK_LIST_INIT_ZERO;
facetize_log_hooks_silence(&saved_hooks);
int prep_status = rt_gettree(rtip, object_name);
if (prep_status == 0)
rt_prep_parallel(rtip, 1);
facetize_log_hooks_restore(&saved_hooks);
if (prep_status != 0 || !rtip->stats.nsolids || !rtip->stats.nregions) {
rt_i_destroy(rtip);
return -1L;
}
struct rt_crofton_params parameters = {};
parameters.n_rays = 0;
parameters.stability_mm = 0.05;
parameters.time_ms = 2000.0;
int crossings = rt_crofton_shoot(surface_area, volume, NULL, NULL, NULL,
NULL, NULL, rtip, &parameters, focus_min, focus_max);
rt_i_destroy(rtip);
return (crossings >= 0) ? (long)crossings : -1L;
}
// Local Variables:
// tab-width: 8
// mode: C++
// c-basic-offset: 4
// indent-tabs-mode: t
// c-file-style: "stroustrup"
// End:
// ex: shiftwidth=4 tabstop=8
+28
View File
@@ -0,0 +1,28 @@
/* V A L I D A T I O N . H
* BRL-CAD
*
* Copyright (c) 2026 United States Government as represented by
* the U.S. Army Research Laboratory.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*/
#ifndef LIBGED_FACETIZE_VALIDATION_H
#define LIBGED_FACETIZE_VALIDATION_H
#include "bu/hook.h"
#include "rt/db_instance.h"
void
facetize_log_hooks_silence(struct bu_hook_list *saved_hooks);
void
facetize_log_hooks_restore(struct bu_hook_list *saved_hooks);
long
facetize_csg_metrics(struct db_i *dbip, const char *object_name,
double *surface_area, double *volume);
#endif /* LIBGED_FACETIZE_VALIDATION_H */
+501 -39
View File
@@ -24,29 +24,230 @@
#include "common.h"
#include <algorithm>
#include <sstream>
#include <errno.h>
#if defined(HAVE_PTHREAD_H) && defined(HAVE_SIGNAL_H)
# include <pthread.h>
# include <signal.h>
# include <time.h>
#endif
#include <string.h>
#include "bu/log.h"
#include "./worker.h"
const char FACETIZE_WORKER_RESULT_OBJECT[] = "facetize_worker_result";
static const char FACETIZE_REQUEST_MAGIC[] = "FACETIZE_REQUEST";
static const char FACETIZE_COMMIT_MAGIC[] = "FACETIZE_COMMIT";
static const char FACETIZE_RESULT_MAGIC[] = "FACETIZE_RESULT";
static const char FACETIZE_WRITE_READY_MAGIC[] = "FACETIZE_WRITE_READY";
static const char FACETIZE_REGION_WRITE_READY_MAGIC[] =
"FACETIZE_REGION_WRITE_READY";
static const char FACETIZE_WRITE_PROCEED_MAGIC[] = "FACETIZE_WRITE_PROCEED";
static const char FACETIZE_WRITE_STARTED_MAGIC[] = "FACETIZE_WRITE_STARTED";
static const char FACETIZE_WRITE_DONE_MAGIC[] = "FACETIZE_WRITE_DONE";
static const size_t FACETIZE_REQUEST_NAME_MAX = 1024u * 1024u;
static const size_t FACETIZE_PROTOCOL_HEADER_SIZE = 128u;
static const char FACETIZE_CSG_RESULT_MAGIC[] = "FACETIZE_CSG_RESULT";
static const unsigned int FACETIZE_PROTOCOL_VERSION = 3u;
static const size_t FACETIZE_PROTOCOL_FIELD_MAX = 1024u * 1024u;
static const size_t FACETIZE_PROTOCOL_LIST_MAX = 65536u;
static const size_t FACETIZE_PROTOCOL_HEADER_SIZE = 256u;
static const size_t FACETIZE_MIB = 1024u * 1024u;
static const size_t FACETIZE_MIN_MEMORY_HEADROOM = 1024u * FACETIZE_MIB;
static const size_t FACETIZE_MIN_WORKER_RESERVE = 512u * FACETIZE_MIB;
static const size_t FACETIZE_MEMORY_HEADROOM_DIVISOR = 4u;
static const size_t FACETIZE_AUTOMATIC_WORKER_LIMIT = 2u;
class FacetizePipeWriteGuard
{
public:
FacetizePipeWriteGuard()
{
#if defined(HAVE_PTHREAD_H) && defined(HAVE_SIGNAL_H) && defined(SIGPIPE)
sigemptyset(&blocked_signals);
sigaddset(&blocked_signals, SIGPIPE);
active = pthread_sigmask(SIG_BLOCK, &blocked_signals,
&original_mask) == 0;
sigset_t pending_signals;
if (active && sigpending(&pending_signals) == 0)
had_pending_sigpipe = sigismember(&pending_signals, SIGPIPE) == 1;
#endif
}
~FacetizePipeWriteGuard()
{
#if defined(HAVE_PTHREAD_H) && defined(HAVE_SIGNAL_H) && defined(SIGPIPE)
if (!active)
return;
int saved_errno = errno;
sigset_t pending_signals;
bool have_new_sigpipe = !had_pending_sigpipe &&
sigpending(&pending_signals) == 0 &&
sigismember(&pending_signals, SIGPIPE) == 1;
if (have_new_sigpipe) {
struct timespec timeout = {0, 0};
while (sigtimedwait(&blocked_signals, NULL, &timeout) < 0 &&
errno == EINTR)
;
}
(void)pthread_sigmask(SIG_SETMASK, &original_mask, NULL);
errno = saved_errno;
#endif
}
private:
#if defined(HAVE_PTHREAD_H) && defined(HAVE_SIGNAL_H) && defined(SIGPIPE)
sigset_t blocked_signals;
sigset_t original_mask;
bool active = false;
bool had_pending_sigpipe = false;
#endif
};
static bool
facetize_send_result(FILE *response_stream, const char *message_type, int result)
facetize_send_field(FILE *stream, const char *field, size_t field_length)
{
return stream && field &&
fwrite(field, 1, field_length, stream) == field_length &&
fputc('\n', stream) != EOF;
}
static bool
facetize_send_sized_field(FILE *stream, const std::string &field)
{
if (!stream || field.size() > FACETIZE_PROTOCOL_FIELD_MAX)
return false;
return fprintf(stream, "%zu\n", field.size()) >= 0 &&
facetize_send_field(stream, field.c_str(), field.size());
}
static FacetizeWorkerReadResult
facetize_read_header(FILE *stream, char *header, size_t header_size)
{
if (!stream || !header || header_size < 2)
return FacetizeWorkerReadResult::Error;
if (!bu_fgets(header, header_size, stream))
return feof(stream) ? FacetizeWorkerReadResult::End :
FacetizeWorkerReadResult::Error;
size_t header_length = strlen(header);
return (header_length && header[header_length - 1] == '\n') ?
FacetizeWorkerReadResult::Request : FacetizeWorkerReadResult::Error;
}
static bool
facetize_read_field(FILE *stream, size_t field_length, std::string &field)
{
field.assign(field_length, '\0');
if ((!field_length ||
fread(&field[0], 1, field_length, stream) == field_length) &&
fgetc(stream) == '\n')
return true;
field.clear();
return false;
}
static bool
facetize_read_sized_field(FILE *stream, std::string &field)
{
field.clear();
char header[FACETIZE_PROTOCOL_HEADER_SIZE];
if (facetize_read_header(stream, header, sizeof(header)) !=
FacetizeWorkerReadResult::Request)
return false;
size_t field_length = 0;
std::istringstream header_stream(header);
if (!(header_stream >> field_length) ||
!(header_stream >> std::ws).eof() ||
field_length > FACETIZE_PROTOCOL_FIELD_MAX)
return false;
return facetize_read_field(stream, field_length, field);
}
static bool
facetize_operation_valid(FacetizeWorkerOperation operation)
{
return operation == FacetizeWorkerOperation::TessellatePrimitive ||
operation == FacetizeWorkerOperation::ValidateCsg ||
operation == FacetizeWorkerOperation::EvaluateRegion ||
operation == FacetizeWorkerOperation::NmgBooleanToBot ||
operation == FacetizeWorkerOperation::NmgBooleanToNmg;
}
static bool
facetize_send_result(FILE *response_stream, const char *message_type, int result,
size_t resident_size)
{
if (!response_stream)
return false;
return fprintf(response_stream, "%s %d\n", message_type, result) >= 0 &&
fflush(response_stream) == 0;
return fprintf(response_stream, "%s %d %zu\n", message_type, result,
resident_size) >= 0 && fflush(response_stream) == 0;
}
FacetizeWorkerPolicy::FacetizeWorkerPolicy(size_t requested_workers,
size_t work_count, size_t available_cpus, size_t total_memory,
size_t available_memory)
{
available_cpus = std::max((size_t)1, available_cpus);
size_t worker_limit = requested_workers ? requested_workers :
FACETIZE_AUTOMATIC_WORKER_LIMIT;
worker_limit = std::min(worker_limit, available_cpus);
if (work_count)
worker_limit = std::min(worker_limit, work_count);
if (total_memory && available_memory) {
memory_headroom = std::max(FACETIZE_MIN_MEMORY_HEADROOM,
total_memory / FACETIZE_MEMORY_HEADROOM_DIVISOR);
if (available_memory > memory_headroom) {
size_t memory_workers = (available_memory - memory_headroom) /
FACETIZE_MIN_WORKER_RESERVE;
worker_limit = std::min(worker_limit,
std::max((size_t)1, memory_workers));
} else {
worker_limit = 1;
}
} else {
worker_limit = 1;
}
workers = std::max((size_t)1, worker_limit);
worker_threads = std::max((size_t)1, available_cpus / workers);
}
size_t
FacetizeWorkerPolicy::worker_count() const
{
return workers;
}
size_t
FacetizeWorkerPolicy::threads_per_worker() const
{
return worker_threads;
}
bool
FacetizeWorkerPolicy::can_dispatch(size_t active_workers,
size_t available_memory, size_t observed_worker_resident) const
{
if (!active_workers)
return true;
if (workers == 1 || !available_memory || !memory_headroom)
return false;
size_t worker_reserve = std::max(FACETIZE_MIN_WORKER_RESERVE,
observed_worker_resident);
if (worker_reserve > SIZE_MAX - memory_headroom)
return false;
return available_memory > memory_headroom + worker_reserve;
}
void
@@ -57,19 +258,116 @@ FacetizeWorkerClient::reset(FILE *stream)
}
bool
FacetizeWorkerClient::send_request(const char *object_name)
FacetizePrimitiveSettings::empty() const
{
if (!request_stream || !object_name || !object_name[0])
return methods.empty() && method_options.empty() &&
cache_directory.empty() && point_limit == 0;
}
bool
FacetizeRegionSettings::empty() const
{
return !no_empty && !no_fixup && !tolerate_failures;
}
bool
FacetizeWorkerRequest::valid() const
{
if (!facetize_operation_valid(operation) || input_names.empty() ||
input_names.size() > FACETIZE_PROTOCOL_LIST_MAX ||
primitive.methods.size() > FACETIZE_PROTOCOL_LIST_MAX ||
primitive.method_options.size() > FACETIZE_PROTOCOL_LIST_MAX ||
output_name.size() > FACETIZE_PROTOCOL_FIELD_MAX ||
primitive.cache_directory.size() > FACETIZE_PROTOCOL_FIELD_MAX ||
primitive.point_limit < 0)
return false;
for (const std::string &input_name : input_names)
if (input_name.empty() || input_name.size() > FACETIZE_PROTOCOL_FIELD_MAX)
return false;
for (const std::string &method : primitive.methods)
if (method.empty() || method.size() > FACETIZE_PROTOCOL_FIELD_MAX)
return false;
for (const std::string &options : primitive.method_options)
if (options.empty() || options.size() > FACETIZE_PROTOCOL_FIELD_MAX)
return false;
switch (operation) {
case FacetizeWorkerOperation::TessellatePrimitive:
return input_names.size() == 1 && output_name.empty() &&
region.empty();
case FacetizeWorkerOperation::ValidateCsg:
return input_names.size() == 1 && output_name.empty() &&
primitive.empty() && region.empty();
case FacetizeWorkerOperation::EvaluateRegion:
return input_names.size() == 1 && output_name.empty() &&
primitive.empty();
case FacetizeWorkerOperation::NmgBooleanToBot:
case FacetizeWorkerOperation::NmgBooleanToNmg:
return !output_name.empty() && primitive.empty() && region.empty();
}
return false;
}
std::string
FacetizeWorkerRequest::label() const
{
if (!output_name.empty())
return output_name;
return input_names.empty() ? "(unknown)" : input_names.front();
}
bool
FacetizeWorkerClient::send_request(const FacetizeWorkerRequest &request)
{
if (!request_stream || !request.valid())
return false;
FacetizePipeWriteGuard write_guard;
if (fprintf(request_stream, "%s %u %d %zu %zu %zu %d %d %d %d\n",
FACETIZE_REQUEST_MAGIC, FACETIZE_PROTOCOL_VERSION,
static_cast<int>(request.operation), request.input_names.size(),
request.primitive.methods.size(),
request.primitive.method_options.size(),
request.primitive.point_limit, request.region.no_empty ? 1 : 0,
request.region.no_fixup ? 1 : 0,
request.region.tolerate_failures ? 1 : 0) < 0 ||
!facetize_send_sized_field(request_stream,
request.primitive.cache_directory) ||
!facetize_send_sized_field(request_stream, request.output_name))
return false;
for (const std::string &input_name : request.input_names)
if (!facetize_send_sized_field(request_stream, input_name))
return false;
for (const std::string &method : request.primitive.methods)
if (!facetize_send_sized_field(request_stream, method))
return false;
for (const std::string &options : request.primitive.method_options)
if (!facetize_send_sized_field(request_stream, options))
return false;
return fflush(request_stream) == 0;
}
bool
FacetizeWorkerClient::send_commit(const char *result_file,
const char *object_name, size_t payload_size)
{
if (!request_stream || !result_file || !result_file[0] ||
!object_name || !object_name[0])
return false;
size_t file_length = strlen(result_file);
size_t name_length = strlen(object_name);
if (name_length > FACETIZE_REQUEST_NAME_MAX)
if (file_length > FACETIZE_PROTOCOL_FIELD_MAX ||
name_length > FACETIZE_PROTOCOL_FIELD_MAX)
return false;
return fprintf(request_stream, "%s %zu\n", FACETIZE_REQUEST_MAGIC,
name_length) >= 0 &&
fwrite(object_name, 1, name_length, request_stream) == name_length &&
fputc('\n', request_stream) != EOF && fflush(request_stream) == 0;
FacetizePipeWriteGuard write_guard;
return fprintf(request_stream, "%s %zu %zu %zu\n",
FACETIZE_COMMIT_MAGIC, file_length, name_length, payload_size) >= 0 &&
facetize_send_field(request_stream, result_file, file_length) &&
facetize_send_field(request_stream, object_name, name_length) &&
fflush(request_stream) == 0;
}
bool
@@ -78,6 +376,7 @@ FacetizeWorkerClient::send_write_proceed()
if (!request_stream)
return false;
FacetizePipeWriteGuard write_guard;
return fprintf(request_stream, "%s\n", FACETIZE_WRITE_PROCEED_MAGIC) >= 0 &&
fflush(request_stream) == 0;
}
@@ -100,26 +399,62 @@ FacetizeWorkerClient::consume_output(const char *data, size_t data_size,
std::string message_type;
int parsed_result = BRLCAD_ERROR;
size_t parsed_payload_size = 0;
size_t parsed_resident_size = 0;
long parsed_crossings = -1;
int parsed_tolerated_failures = 0;
double parsed_surface_area = -1.0;
double parsed_volume = -1.0;
line_stream >> message_type;
if (message_type == FACETIZE_WRITE_READY_MAGIC &&
(line_stream >> parsed_payload_size) &&
(line_stream >> parsed_payload_size >> parsed_resident_size) &&
(line_stream >> std::ws).eof()) {
status.write_ready = true;
status.payload_size = parsed_payload_size;
status.resident_size = parsed_resident_size;
continue;
}
if (message_type == FACETIZE_REGION_WRITE_READY_MAGIC &&
(line_stream >> parsed_payload_size >> parsed_resident_size >>
parsed_tolerated_failures) &&
(line_stream >> std::ws).eof() &&
parsed_tolerated_failures >= 0) {
status.write_ready = true;
status.payload_size = parsed_payload_size;
status.resident_size = parsed_resident_size;
status.tolerated_failures = parsed_tolerated_failures;
continue;
}
if (message_type == FACETIZE_WRITE_STARTED_MAGIC &&
(line_stream >> std::ws).eof()) {
status.write_started = true;
continue;
}
if (message_type == FACETIZE_WRITE_DONE_MAGIC &&
(line_stream >> parsed_result) &&
(line_stream >> parsed_result >> parsed_resident_size) &&
(line_stream >> std::ws).eof()) {
status.result = parsed_result;
status.resident_size = parsed_resident_size;
status.write_done = true;
status.result_received = true;
continue;
}
if (message_type == FACETIZE_RESULT_MAGIC &&
(line_stream >> parsed_result) &&
(line_stream >> parsed_result >> parsed_resident_size) &&
(line_stream >> std::ws).eof()) {
status.result = parsed_result;
status.resident_size = parsed_resident_size;
status.result_received = true;
continue;
}
if (message_type == FACETIZE_CSG_RESULT_MAGIC &&
(line_stream >> parsed_result >> parsed_crossings >>
parsed_surface_area >> parsed_volume >> parsed_resident_size) &&
(line_stream >> std::ws).eof()) {
status.result = parsed_result;
status.csg_crossings = parsed_crossings;
status.csg_surface_area = parsed_surface_area;
status.csg_volume = parsed_volume;
status.resident_size = parsed_resident_size;
status.result_received = true;
continue;
}
@@ -134,35 +469,124 @@ FacetizeWorkerServer::FacetizeWorkerServer(FILE *input, FILE *output) :
}
FacetizeWorkerReadResult
FacetizeWorkerServer::receive_request(std::string &object_name)
FacetizeWorkerServer::receive_request(FacetizeWorkerRequest &request)
{
object_name.clear();
request = FacetizeWorkerRequest();
if (!request_stream)
return FacetizeWorkerReadResult::Error;
char header[FACETIZE_PROTOCOL_HEADER_SIZE];
if (!bu_fgets(header, sizeof(header), request_stream))
return feof(request_stream) ? FacetizeWorkerReadResult::End :
FacetizeWorkerReadResult::Error;
FacetizeWorkerReadResult read_result = facetize_read_header(request_stream,
header, sizeof(header));
if (read_result != FacetizeWorkerReadResult::Request)
return read_result;
size_t header_length = strlen(header);
if (!header_length || header[header_length - 1] != '\n')
return FacetizeWorkerReadResult::Error;
size_t name_length = 0;
unsigned int protocol_version = 0;
int operation_value = 0;
size_t input_count = 0;
size_t method_count = 0;
size_t method_option_count = 0;
int point_limit = 0;
int no_empty = 0;
int no_fixup = 0;
int tolerate_failures = 0;
std::string message_type;
std::istringstream header_stream(header);
if (!(header_stream >> message_type) ||
message_type != FACETIZE_REQUEST_MAGIC ||
!(header_stream >> name_length) ||
!(header_stream >> std::ws).eof() || !name_length ||
name_length > FACETIZE_REQUEST_NAME_MAX)
!(header_stream >> protocol_version) ||
protocol_version != FACETIZE_PROTOCOL_VERSION ||
!(header_stream >> operation_value >> input_count >> method_count >>
method_option_count >> point_limit >> no_empty >> no_fixup >>
tolerate_failures) ||
!(header_stream >> std::ws).eof() || !input_count ||
input_count > FACETIZE_PROTOCOL_LIST_MAX ||
method_count > FACETIZE_PROTOCOL_LIST_MAX ||
method_option_count > FACETIZE_PROTOCOL_LIST_MAX || point_limit < 0 ||
(no_empty != 0 && no_empty != 1) ||
(no_fixup != 0 && no_fixup != 1) ||
(tolerate_failures != 0 && tolerate_failures != 1))
return FacetizeWorkerReadResult::Error;
object_name.assign(name_length, '\0');
if (fread(&object_name[0], 1, name_length, request_stream) != name_length ||
fgetc(request_stream) != '\n') {
object_name.clear();
request.operation = static_cast<FacetizeWorkerOperation>(operation_value);
request.primitive.point_limit = point_limit;
request.region.no_empty = no_empty != 0;
request.region.no_fixup = no_fixup != 0;
request.region.tolerate_failures = tolerate_failures != 0;
if (!facetize_read_sized_field(request_stream,
request.primitive.cache_directory) ||
!facetize_read_sized_field(request_stream, request.output_name)) {
request = FacetizeWorkerRequest();
return FacetizeWorkerReadResult::Error;
}
request.input_names.reserve(input_count);
for (size_t i = 0; i < input_count; i++) {
std::string input_name;
if (!facetize_read_sized_field(request_stream, input_name)) {
request = FacetizeWorkerRequest();
return FacetizeWorkerReadResult::Error;
}
request.input_names.push_back(input_name);
}
request.primitive.methods.reserve(method_count);
for (size_t i = 0; i < method_count; i++) {
std::string method;
if (!facetize_read_sized_field(request_stream, method)) {
request = FacetizeWorkerRequest();
return FacetizeWorkerReadResult::Error;
}
request.primitive.methods.push_back(method);
}
request.primitive.method_options.reserve(method_option_count);
for (size_t i = 0; i < method_option_count; i++) {
std::string options;
if (!facetize_read_sized_field(request_stream, options)) {
request = FacetizeWorkerRequest();
return FacetizeWorkerReadResult::Error;
}
request.primitive.method_options.push_back(options);
}
if (!request.valid()) {
request = FacetizeWorkerRequest();
return FacetizeWorkerReadResult::Error;
}
return FacetizeWorkerReadResult::Request;
}
FacetizeWorkerReadResult
FacetizeWorkerServer::receive_commit(FacetizeCommitRequest &request)
{
request = FacetizeCommitRequest();
if (!request_stream)
return FacetizeWorkerReadResult::Error;
char header[FACETIZE_PROTOCOL_HEADER_SIZE];
FacetizeWorkerReadResult read_result = facetize_read_header(request_stream,
header, sizeof(header));
if (read_result != FacetizeWorkerReadResult::Request)
return read_result;
size_t file_length = 0;
size_t name_length = 0;
std::string message_type;
std::istringstream header_stream(header);
if (!(header_stream >> message_type) ||
message_type != FACETIZE_COMMIT_MAGIC ||
!(header_stream >> file_length >> name_length >> request.payload_size) ||
!(header_stream >> std::ws).eof() || !file_length || !name_length ||
file_length > FACETIZE_PROTOCOL_FIELD_MAX ||
name_length > FACETIZE_PROTOCOL_FIELD_MAX)
return FacetizeWorkerReadResult::Error;
if (!facetize_read_field(request_stream, file_length,
request.result_file) ||
!facetize_read_field(request_stream, name_length,
request.object_name)) {
request = FacetizeCommitRequest();
return FacetizeWorkerReadResult::Error;
}
@@ -170,13 +594,27 @@ FacetizeWorkerServer::receive_request(std::string &object_name)
}
bool
FacetizeWorkerServer::send_write_ready(size_t payload_size)
FacetizeWorkerServer::send_write_ready(size_t payload_size,
size_t resident_size)
{
if (!response_stream)
return false;
return fprintf(response_stream, "%s %zu\n", FACETIZE_WRITE_READY_MAGIC,
payload_size) >= 0 && fflush(response_stream) == 0;
return fprintf(response_stream, "%s %zu %zu\n",
FACETIZE_WRITE_READY_MAGIC, payload_size, resident_size) >= 0 &&
fflush(response_stream) == 0;
}
bool
FacetizeWorkerServer::send_region_write_ready(size_t payload_size,
size_t resident_size, int tolerated_failures)
{
if (!response_stream || tolerated_failures < 0)
return false;
return fprintf(response_stream, "%s %zu %zu %d\n",
FACETIZE_REGION_WRITE_READY_MAGIC, payload_size, resident_size,
tolerated_failures) >= 0 && fflush(response_stream) == 0;
}
bool
@@ -194,16 +632,40 @@ FacetizeWorkerServer::receive_write_proceed()
}
bool
FacetizeWorkerServer::send_tessellation_result(int result)
FacetizeWorkerServer::send_write_started()
{
return facetize_send_result(response_stream, FACETIZE_RESULT_MAGIC, result);
if (!response_stream)
return false;
return fprintf(response_stream, "%s\n", FACETIZE_WRITE_STARTED_MAGIC) >= 0 &&
fflush(response_stream) == 0;
}
bool
FacetizeWorkerServer::send_write_result(int result)
FacetizeWorkerServer::send_tessellation_result(int result,
size_t resident_size)
{
return facetize_send_result(response_stream, FACETIZE_RESULT_MAGIC, result,
resident_size);
}
bool
FacetizeWorkerServer::send_write_result(int result, size_t resident_size)
{
return facetize_send_result(response_stream, FACETIZE_WRITE_DONE_MAGIC,
result);
result, resident_size);
}
bool
FacetizeWorkerServer::send_csg_result(int result, long crossings,
double surface_area, double volume, size_t resident_size)
{
if (!response_stream)
return false;
return fprintf(response_stream, "%s %d %ld %.17g %.17g %zu\n",
FACETIZE_CSG_RESULT_MAGIC, result, crossings, surface_area, volume,
resident_size) >= 0 && fflush(response_stream) == 0;
}
// Local Variables:
+89 -5
View File
@@ -19,12 +19,88 @@
#include "bu/defines.h"
extern const char FACETIZE_WORKER_RESULT_OBJECT[];
struct FacetizeWorkerStatus {
bool write_ready = false;
bool write_started = false;
bool write_done = false;
bool result_received = false;
int result = BRLCAD_ERROR;
size_t payload_size = 0;
size_t resident_size = 0;
long csg_crossings = -1;
double csg_surface_area = -1.0;
double csg_volume = -1.0;
int tolerated_failures = 0;
};
struct FacetizeCommitRequest {
std::string result_file;
std::string object_name;
size_t payload_size = 0;
};
enum class FacetizeWorkerOperation {
TessellatePrimitive = 1,
ValidateCsg = 2,
EvaluateRegion = 3,
NmgBooleanToBot = 4,
NmgBooleanToNmg = 5
};
struct FacetizePrimitiveSettings {
std::vector<std::string> methods;
std::vector<std::string> method_options;
std::string cache_directory;
int point_limit = 0;
bool empty() const;
};
struct FacetizeRegionSettings {
bool no_empty = false;
bool no_fixup = false;
bool tolerate_failures = false;
bool empty() const;
};
struct FacetizeWorkerRequest {
FacetizeWorkerOperation operation =
FacetizeWorkerOperation::TessellatePrimitive;
std::vector<std::string> input_names;
std::string output_name;
FacetizePrimitiveSettings primitive;
FacetizeRegionSettings region;
bool valid() const;
std::string label() const;
};
/**
* Resource limits for a pool of independent facetize workers.
*
* A zero requested_workers selects the conservative automatic limit. Memory
* values are current system-wide byte counts; zero means the platform could
* not supply that measurement. Unknown memory disables outer parallelism.
*/
class FacetizeWorkerPolicy
{
public:
FacetizeWorkerPolicy(size_t requested_workers, size_t work_count,
size_t available_cpus, size_t total_memory,
size_t available_memory);
size_t worker_count() const;
size_t threads_per_worker() const;
bool can_dispatch(size_t active_workers, size_t available_memory,
size_t observed_worker_resident) const;
private:
size_t workers = 1;
size_t worker_threads = 1;
size_t memory_headroom = 0;
};
/**
@@ -41,7 +117,9 @@ class FacetizeWorkerClient
FacetizeWorkerClient() = default;
void reset(FILE *request_stream);
bool send_request(const char *object_name);
bool send_request(const FacetizeWorkerRequest &request);
bool send_commit(const char *result_file, const char *object_name,
size_t payload_size);
bool send_write_proceed();
void consume_output(const char *data, size_t data_size,
FacetizeWorkerStatus &status,
@@ -69,11 +147,17 @@ class FacetizeWorkerServer
public:
FacetizeWorkerServer(FILE *request_stream, FILE *response_stream);
FacetizeWorkerReadResult receive_request(std::string &object_name);
bool send_write_ready(size_t payload_size);
FacetizeWorkerReadResult receive_request(FacetizeWorkerRequest &request);
FacetizeWorkerReadResult receive_commit(FacetizeCommitRequest &request);
bool send_write_ready(size_t payload_size, size_t resident_size);
bool send_region_write_ready(size_t payload_size, size_t resident_size,
int tolerated_failures);
bool receive_write_proceed();
bool send_tessellation_result(int result);
bool send_write_result(int result);
bool send_write_started();
bool send_tessellation_result(int result, size_t resident_size);
bool send_write_result(int result, size_t resident_size);
bool send_csg_result(int result, long crossings, double surface_area,
double volume, size_t resident_size);
private:
FILE *request_stream = NULL;
+29 -37
View File
@@ -483,19 +483,6 @@ _ged_pnts_tri_cmd_ballpivot(void *bs, int argc, const char **argv)
return wret;
}
/* custom bu_opt for size_t threads in spsr */
static int
_pnts_opt_size_t(struct bu_vls *UNUSED(msg), size_t UNUSED(argc), const char **argv, void *set_c)
{
size_t *t = (size_t *)set_c;
int tmp = 0;
const char *a = argv[0];
if (bu_opt_int(NULL, 1, &a, &tmp) < 0) return -1;
if (tmp < 0) return -1;
*t = (size_t)tmp;
return 1;
}
/* tri spsr */
static int
_ged_pnts_tri_cmd_spsr(void *bs, int argc, const char **argv)
@@ -508,29 +495,23 @@ _ged_pnts_tri_cmd_spsr(void *bs, int argc, const char **argv)
struct bg_3d_spsr_opts opts = BG_3D_SPSR_OPTS_DEFAULT;
int print_help = 0;
struct bu_opt_desc d[23];
BU_OPT(d[0], "h", "help", "", NULL, &print_help, "Print help and exit");
BU_OPT(d[1], "", "degree", "#", &bu_opt_int, &opts.degree, "Finite element degree");
BU_OPT(d[2], "", "btype", "#", &bu_opt_int, &opts.btype, "Boundary type (1:FREE, 2:NEUMANN, 3:DIRICHLET)");
BU_OPT(d[3], "", "depth", "#", &bu_opt_int, &opts.depth, "Max reconstruction depth");
BU_OPT(d[4], "", "kerneldepth", "#", &bu_opt_int, &opts.kerneldepth, "Kernel depth");
BU_OPT(d[5], "", "iterations", "#", &bu_opt_int, &opts.iterations, "Solver iterations");
BU_OPT(d[6], "", "full-depth", "#", &bu_opt_int, &opts.full_depth, "Full depth");
BU_OPT(d[7], "", "base-depth", "#", &bu_opt_int, &opts.base_depth, "Coarse MG depth");
BU_OPT(d[8], "", "base-vcycles", "#", &bu_opt_int, &opts.baseVcycles, "Coarse MG v-cycles");
BU_OPT(d[9], "", "max-mem", "#", &bu_opt_int, &opts.max_memory_GB, "Max memory (GB)");
BU_OPT(d[10], "", "threads", "#", _pnts_opt_size_t, &opts.threads, "Threads to use");
BU_OPT(d[11], "", "samples-per-node", "#", &bu_opt_fastf_t, &opts.samples_per_node,"Min samples per node");
BU_OPT(d[12], "", "scale", "#", &bu_opt_fastf_t, &opts.scale, "Scale factor");
BU_OPT(d[13], "", "width", "#", &bu_opt_fastf_t, &opts.width, "Voxel width");
BU_OPT(d[14], "", "confidence", "#", &bu_opt_fastf_t, &opts.confidence, "Normal confidence exponent");
BU_OPT(d[15], "", "confidence-bias", "#", &bu_opt_fastf_t, &opts.confidence_bias, "Normal confidence bias exponent");
BU_OPT(d[16], "", "cg-accuracy", "#", &bu_opt_fastf_t, &opts.cgsolver_accuracy,"CG solver accuracy");
BU_OPT(d[17], "", "point-weight", "#", &bu_opt_fastf_t, &opts.point_weight, "Interpolation weight");
BU_OPT(d[18], "", "nonmanifold", "#", &bu_opt_int, &opts.nonManifold, "NonManifold (0/1)");
BU_OPT(d[19], "", "linearfit", "#", &bu_opt_int, &opts.linearFit, "Linear Fit (0/1)");
BU_OPT(d[20], "", "exact", "#", &bu_opt_int, &opts.exact, "Exact interpolation (0/1)");
BU_OPT_NULL(d[21]);
struct bu_opt_desc d[16];
BU_OPT(d[0], "h", "help", "", NULL, &print_help, "Print help and exit");
BU_OPT(d[1], "", "depth", "#", &bu_opt_int, &opts.depth, "Max reconstruction depth");
BU_OPT(d[2], "", "kerneldepth", "#", &bu_opt_int, &opts.kerneldepth, "Kernel depth");
BU_OPT(d[3], "", "iterations", "#", &bu_opt_int, &opts.iterations, "Solver iterations");
BU_OPT(d[4], "", "full-depth", "#", &bu_opt_int, &opts.full_depth, "Full depth");
BU_OPT(d[5], "", "base-depth", "#", &bu_opt_int, &opts.base_depth, "Coarse MG depth");
BU_OPT(d[6], "", "base-vcycles", "#", &bu_opt_int, &opts.baseVcycles, "Coarse MG v-cycles");
BU_OPT(d[7], "", "samples-per-node", "#", &bu_opt_fastf_t, &opts.samples_per_node, "Min samples per node");
BU_OPT(d[8], "", "scale", "#", &bu_opt_fastf_t, &opts.scale, "Scale factor");
BU_OPT(d[9], "", "width", "#", &bu_opt_fastf_t, &opts.width, "Voxel width");
BU_OPT(d[10], "", "cg-accuracy", "#", &bu_opt_fastf_t, &opts.cgsolver_accuracy,"CG solver accuracy");
BU_OPT(d[11], "", "point-weight", "#", &bu_opt_fastf_t, &opts.point_weight, "Interpolation weight");
BU_OPT(d[12], "", "nonmanifold", "#", &bu_opt_int, &opts.nonManifold, "NonManifold (0/1)");
BU_OPT(d[13], "", "linearfit", "#", &bu_opt_int, &opts.linearFit, "Linear Fit (0/1)");
BU_OPT(d[14], "", "exact", "#", &bu_opt_int, &opts.exact, "Exact interpolation (0/1)");
BU_OPT_NULL(d[15]);
argc -= (argc>0); argv += (argc>0); // skip "spsr"
if (argc < 1) {
@@ -581,7 +562,18 @@ _ged_pnts_tri_cmd_spsr(void *bs, int argc, const char **argv)
point_t *overts = NULL;
int nverts = 0;
int sret = bg_3d_spsr(&faces, &nfaces, &overts, &nverts, (const point_t *)ipts, (const vect_t *)inrms, pcnt, &opts);
std::vector<struct bg_3d_spsr_sample> samples(
static_cast<size_t>(pcnt));
for (size_t i = 0; i < samples.size(); i++) {
VMOVE(samples[i].point, ipts[i]);
VMOVE(samples[i].normal, inrms[i]);
}
struct bg_3d_spsr_adaptive_opts adaptive_options =
BG_3D_SPSR_ADAPTIVE_OPTS_DEFAULT;
adaptive_options.solver = opts;
adaptive_options.max_refinement_passes = 0;
int sret = bg_3d_spsr_adaptive(&faces, &nfaces, &overts, &nverts,
samples.data(), samples.size(), &adaptive_options, NULL, NULL, NULL);
// free inputs
if (ipts)
+20
View File
@@ -6,6 +6,24 @@ brlcad_find_package(OPENNURBS REQUIRED SYSPATTERN openNURBS)
brlcad_find_package(REGEX REQUIRED)
find_package_gte(REQUIRED)
set(OPENQMC_LIBS)
if(BRLCAD_ENABLE_QMC)
brlcad_find_package(OpenQMC CONFIG)
if(OpenQMC_FOUND AND TARGET OpenQMC::OpenQMC)
add_definitions(-DBRLCAD_ENABLE_QMC)
set(OPENQMC_LIBS OpenQMC::OpenQMC)
else()
message(
WARNING
"QMC requested, but OpenQMC was not found - disabling"
)
set(
BRLCAD_ENABLE_QMC OFF CACHE BOOL
"Disabled because OpenQMC was not found" FORCE
)
endif()
endif()
if(BRLCAD_ENABLE_OPENCL)
add_definitions(-DUSE_OPENCL)
endif(BRLCAD_ENABLE_OPENCL)
@@ -427,6 +445,7 @@ endif(BRLCAD_ENABLE_SPR)
set(LIBRT_PUBLIC_LIBS ${librt_deps} OPENNURBS::OPENNURBS)
set(
LIBRT_PRIVATE_LIBS
Threads::Threads
${OPENCL_LIBS}
mmesh::mmesh
REGEX::REGEX
@@ -434,6 +453,7 @@ set(
${RPCRT_LIB}
${STDCXX_LIBRARIES}
manifold::manifold
${OPENQMC_LIBS}
)
# Include directories needed by librt users
File diff suppressed because it is too large Load Diff
+108
View File
@@ -43,6 +43,18 @@ rel_err(double estimated, double exact)
}
static int
crofton_segments_equal(const struct rt_crofton_segment *left,
const struct rt_crofton_segment *right)
{
return left->ray_id == right->ray_id &&
NEAR_EQUAL(left->thickness, right->thickness, SMALL_FASTF) &&
VNEAR_EQUAL(left->in_point, right->in_point, SMALL_FASTF) &&
VNEAR_EQUAL(left->in_normal, right->in_normal, SMALL_FASTF) &&
VNEAR_EQUAL(left->out_point, right->out_point, SMALL_FASTF) &&
VNEAR_EQUAL(left->out_normal, right->out_normal, SMALL_FASTF);
}
static int
verify_crofton_estimates(void)
{
@@ -379,6 +391,102 @@ run_convergence_case(struct db_i *dbip,
&sample_points, &sample_count,
rtip, &p, NULL, NULL);
run_sec = (double)(bu_gettime() - t0) / 1000000.0;
if (i == 0) {
struct rt_crofton_result samples = RT_CROFTON_RESULT_INIT;
const size_t ray_offset = 4096u;
const size_t continuation_rays = 256u;
struct rt_crofton_params sample_params =
{ray_offset + continuation_rays, 0.0, 0.0};
int sample_ret = rt_crofton_collect(&samples, rtip,
&sample_params, 0, NULL, NULL);
int full_samples_valid = sample_ret > 0 && samples.segments &&
samples.segment_count * 2 == samples.crossing_count &&
samples.ray_count == sample_params.n_rays;
if (!full_samples_valid) {
printf(" %-24s invalid structured Crofton output\\n", label);
failures++;
} else {
for (size_t sample_index = 0;
sample_index < samples.segment_count; sample_index++) {
struct rt_crofton_segment *segment =
&samples.segments[sample_index];
if (segment->thickness <= 0.0 ||
!NEAR_EQUAL(DIST_PNT_PNT(segment->in_point,
segment->out_point), segment->thickness, RT_LEN_TOL) ||
!NEAR_EQUAL(MAGNITUDE(segment->in_normal), 1.0,
VUNITIZE_TOL) ||
!NEAR_EQUAL(MAGNITUDE(segment->out_normal), 1.0,
VUNITIZE_TOL) ||
segment->ray_id >= samples.ray_count) {
printf(" %-24s invalid structured segment\\n", label);
failures++;
break;
}
}
}
struct rt_crofton_result offset_samples =
RT_CROFTON_RESULT_INIT;
struct rt_crofton_params offset_params =
{continuation_rays, 0.0, 0.0};
sample_ret = rt_crofton_collect(&offset_samples, rtip,
&offset_params, ray_offset, NULL, NULL);
int offset_samples_valid = sample_ret > 0 &&
offset_samples.ray_count == offset_params.n_rays;
if (!offset_samples_valid) {
printf(" %-24s invalid offset Crofton output\\n", label);
failures++;
} else {
for (size_t sample_index = 0;
sample_index < offset_samples.segment_count;
sample_index++) {
size_t ray_id = offset_samples.segments[sample_index].ray_id;
if (ray_id < ray_offset ||
ray_id >= ray_offset + offset_params.n_rays) {
printf(" %-24s invalid offset ray identifier\\n",
label);
failures++;
break;
}
}
}
if (full_samples_valid && offset_samples_valid) {
size_t suffix_start = 0;
while (suffix_start < samples.segment_count &&
samples.segments[suffix_start].ray_id < ray_offset)
suffix_start++;
size_t suffix_count = samples.segment_count - suffix_start;
if (suffix_count != offset_samples.segment_count) {
printf(" %-24s random stream continuation count mismatch\\n",
label);
failures++;
} else {
for (size_t sample_index = 0;
sample_index < suffix_count; sample_index++) {
const struct rt_crofton_segment *full_segment =
&samples.segments[suffix_start + sample_index];
const struct rt_crofton_segment *offset_segment =
&offset_samples.segments[sample_index];
if (!crofton_segments_equal(full_segment, offset_segment)) {
printf(" %-24s random stream continuation mismatch\\n",
label);
failures++;
break;
}
}
}
}
rt_crofton_result_free(&samples);
if (samples.segments || samples.segment_count)
failures++;
rt_crofton_result_free(&offset_samples);
if (offset_samples.segments || offset_samples.segment_count)
failures++;
}
rt_i_destroy(rtip);
if (elapsed_total_sec)
-1
View File
@@ -192,7 +192,6 @@ static struct cmdtab mged_cmdtab[] = {
{MGED_CMD_MAGIC, "exists", cmd_ged_plain_wrapper, ged_exec_exists, NULL},
{MGED_CMD_MAGIC, "facedef", f_facedef, GED_FUNC_PTR_NULL, NULL},
{MGED_CMD_MAGIC, "facetize", cmd_ged_plain_wrapper, ged_exec_facetize, NULL},
{MGED_CMD_MAGIC, "facetize_old", cmd_ged_plain_wrapper, ged_exec_facetize_old, NULL},
{MGED_CMD_MAGIC, "fb2pix", cmd_ged_dm_wrapper, ged_exec_fb2pix, NULL},
{MGED_CMD_MAGIC, "fbclear", cmd_ged_dm_wrapper, ged_exec_fbclear, NULL},
{MGED_CMD_MAGIC, "find_arb_edge", cmd_ged_plain_wrapper, ged_exec_find_arb_edge, NULL},
+1 -1
View File
@@ -177,7 +177,7 @@ set mged_help_data(expand) $helplib_data(wdb_expand)
set mged_help_data(extrude) {{#### distance} {extrude dist from face}}
set mged_help_data(eye_pt) $helplib_data(vo_eye)
set mged_help_data(facedef) {{####} {define new face for an arb}}
set mged_help_data(facetize) {{[ -nmhT | [--NMG] [--CM] [--SPSR] ] [old_obj1 | new_obj] [old_obj* ...] [old_objN | new_obj]} {Convert objects to faceted BOT objects. See man page ("man facetize") for all usage and examples}}
set mged_help_data(facetize) {{[options] [old_obj1 | new_obj] [old_obj* ...] [old_objN | new_obj]} {Convert objects to faceted BOT objects. See man page ("man facetize") for all usage and examples}}
set mged_help_data(form) $helplib_data(wdb_form)
set mged_help_data(fracture) {{NMGsolid [prefix]} {fracture an NMG solid into many NMG solids, each containing one face}}
set mged_help_data(g) $helplib_data(wdb_group)