Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 32 additions & 12 deletions bindings/c/clay_c.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3798,7 +3798,7 @@ clay_result try_band(const Compile& compile, eval::Backend& backend, const math:
*out_answered = false;
*out_reach = std::numeric_limits<float>::infinity();
scene::Tape tape;
const scene::CullRegion cull{box.dilated(band + tap_pad)};
const scene::CullRegion cull{box.dilated(band + tap_pad), band};
clay_result r = compile(&cull, &tape);
if (r != CLAY_OK) return r;
// A region that kept NOTHING cannot answer: every probe reads
Expand Down Expand Up @@ -4665,6 +4665,22 @@ clay_result read_region(const float region_min[3], const float region_max[3], co
return CLAY_OK;
}

// How far inside a host's cull region every sample of its lattice lies: the band
// the region was dilated by, as far as the call can see it (scene::CullRegion).
// A host hands clay_eval_grid a brick's cull region and the brick's lattice, so
// this recovers the band clay_brick_cache_cull_region added. Zero when a sample
// sits on or outside the region, which claims nothing.
float lattice_band_in(const math::Aabb& region, const eval::GridQuery& q) {
const kernel::cfloat3 last =
q.origin + kernel::cf3(q.spacing * static_cast<float>(q.nx - 1),
q.spacing * static_cast<float>(q.ny - 1),
q.spacing * static_cast<float>(q.nz - 1));
const float m = std::min({q.origin.x - region.min.x, q.origin.y - region.min.y,
q.origin.z - region.min.z, region.max.x - last.x,
region.max.y - last.y, region.max.z - last.z});
return m > 0.0f ? m : 0.0f;
}

// count * per, computed where it cannot wrap, and required to be exactly the
// caller's capacity. A count is the one argument this boundary cannot check
// against the caller's memory, so it is not inferred.
Expand Down Expand Up @@ -5168,7 +5184,7 @@ scene::Tape compile_request_tape(const clay_document* doc, const clay_brick_requ
ChunkHalf half, scene::LayerId active,
const scene::CullIndex* index, const scene::CullPlan* plan,
scene::TapeCheckpoint* cp) {
scene::CullRegion cull{request_brick_box(req).dilated(req.band)};
scene::CullRegion cull{request_brick_box(req).dilated(req.band), req.band};
const scene::Document& d = doc->doc.document;
if (half == ChunkHalf::Whole)
return cp ? scene::compile_document_resumable(d, cp, &cull, index, plan)
Expand Down Expand Up @@ -5216,9 +5232,12 @@ clay_result eval_requests_in_chunks(const clay_document* doc, const clay_brick_r
}
std::shared_ptr<const scene::CullIndex> index = doc->cull_index();
math::Aabb batch_region;
for (std::size_t i = 0; i < count; ++i)
float batch_band = 0.0f;
for (std::size_t i = 0; i < count; ++i) {
batch_region.expand(request_brick_box(requests[i]).dilated(requests[i].band));
const scene::CullPlan plan = index->plan(batch_region);
batch_band = std::max(batch_band, requests[i].band);
}
const scene::CullPlan plan = index->plan(batch_region, batch_band);
constexpr std::size_t kChunk = 4096;
constexpr bool kWantCheckpoints = !std::is_same_v<std::decay_t<Post>, NoPost>;
std::vector<scene::Tape> tapes;
Expand Down Expand Up @@ -8001,7 +8020,7 @@ void prepare_frontier_seeds(clay_document* doc, const DragFrontier& frontier) {
job.key.spacing * static_cast<float>(job.key.dims[1]),
job.key.spacing * static_cast<float>(job.key.dims[2]));
const math::Aabb box = math::Aabb{lo, lo + size}.dilated(job.key.band);
scene::CullRegion cull{box};
scene::CullRegion cull{box, job.key.band};
scene::Tape prefix;
if (!scene::compile_layer_prefix(d, job.boundary, &prefix, &cull, prep.index.get()))
continue; // values stay empty; phase C skips the job
Expand Down Expand Up @@ -14192,8 +14211,8 @@ clay_result clay_eval_grid(const clay_document* doc, const char* backend,
// revision-cached cull index, so it walks the region's neighbourhood
// rather than the whole document.
std::shared_ptr<const scene::CullIndex> index = doc->cull_index();
const scene::CullPlan plan = index->plan(region);
scene::CullRegion cull{region};
const scene::CullRegion cull{region, lattice_band_in(region, query)};
const scene::CullPlan plan = index->plan(region, cull.band);
scene::Tape tape = scene::compile_document(doc->doc.document, &cull, index.get(), &plan);
return eval_grid_into(tape, backend, query, out_values, out_colors_rgb);
}
Expand Down Expand Up @@ -14389,8 +14408,8 @@ clay_result clay_eval_grid_device(const clay_document* doc, clay_device* device,
const scene::Tape* tape = nullptr;
if (has_region) {
std::shared_ptr<const scene::CullIndex> index = doc->cull_index();
const scene::CullPlan plan = index->plan(region);
scene::CullRegion cull{region};
const scene::CullRegion cull{region, lattice_band_in(region, query)};
const scene::CullPlan plan = index->plan(region, cull.band);
culled = scene::compile_document(doc->doc.document, &cull, index.get(), &plan);
tape = &culled;
} else {
Expand Down Expand Up @@ -15214,7 +15233,7 @@ UniformProbe probe_uniform_seed(const clay_document* doc, const clay_brick_reque
// cull index snapshot and writes only its own brick's slot.
void run_uniform_task(const clay_document* doc, const clay_brick_request& req,
const scene::CullIndex* index, ResumeTask& t) {
scene::CullRegion cull{request_brick_box(req).dilated(req.band)};
scene::CullRegion cull{request_brick_box(req).dilated(req.band), req.band};
scene::Tape suffix;
scene::TapeCheckpoint next;
if (!scene::compile_layer_suffix(t.plan->checkpoint, doc->doc.document, t.plan->appended,
Expand Down Expand Up @@ -15277,8 +15296,9 @@ void run_resume_task(const ResumeRun& run, ResumeTask& t, std::vector<float>& po
run_uniform_task(run.doc, run.requests[t.slot], run.index, t);
return;
}
const math::Aabb box = request_brick_box(run.requests[t.slot]).dilated(run.requests[t.slot].band);
scene::CullRegion cull{box};
const float band = run.requests[t.slot].band;
const math::Aabb box = request_brick_box(run.requests[t.slot]).dilated(band);
scene::CullRegion cull{box, band};
// The checkpoint is PER BRICK where it has frames, because a frame's
// `emits` depends on this brick's own cull. The plan supplies what is
// batch-wide -- the layer and the appended ids -- and the seed the rest.
Expand Down
15 changes: 15 additions & 0 deletions docs/05-claycore-library.md
Original file line number Diff line number Diff line change
Expand Up @@ -1283,6 +1283,21 @@ as `clay_mesh_transform_nonuniform` already documents, because rotating a normal
is right for a similarity and tilts every one of them off the surface under a
squash.

**The per-brick cull widens a squashed placement (#649).** Because the value
can be short of the distance by up to `q = max(s) / min(s)` (the two levels'
ratios multiplied), "the bound is more than band + pad from the brick" no longer
means "the field is more than band + pad there". A brick in between dropped an
item whose field was inside the band: refill samples off `clay_eval_points` by
up to 0.068 on random documents. The cull now widens such a bound by
`(q - 1) · (band + pad + w)` before testing it, where `w` is the rounding and
blend support the bound already carries, and a group by its subtree's widest
term plus `(q - 1)` times its own support (`scene::CullSquash`). The item is
still culled wherever its field cannot reach. That needs the band, so
`scene::CullRegion` carries it next to the region, and `CullIndex::plan` takes
the batch's widest band. A plan made for a narrower band than a region carries is
dropped for a squashed document, which costs the walk and never the field. A
document with no per-axis scale culls exactly as before, whatever the band says.

`kSceneMinor` and `kClaySpaceMinor` move to 16. An older stream loads with
(1, 1, 1), which is what those files always meant; a stream written at an older
minor does not carry the field, so that minor's reader does not desynchronise.
Expand Down
7 changes: 4 additions & 3 deletions include/clay/eval/bake_volume.h
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ inline field::FieldVolume::BrickBlockFill document_block_fill(const scene::Docum
std::size_t sampled = 0, instrs = 0;
for (std::size_t slot = 0; slot < slots && sampled < kProbes;
slot += step, ++sampled) {
scene::CullRegion cr{grid.brick_box(slot).dilated(grid.band)};
scene::CullRegion cr{grid.brick_box(slot).dilated(grid.band), grid.band};
instrs += scene::compile_document(doc, &cr).instrs.size();
}
const std::size_t whole = tape.instrs.size();
Expand Down Expand Up @@ -274,11 +274,12 @@ inline field::FieldVolume::BrickBlockFill document_block_fill(const scene::Docum
for (std::size_t s = 0; s < count; ++s)
window.expand(grid.brick_box(first + s).dilated(grid.band));
const scene::CullIndex& idx = **index;
const scene::CullPlan plan = idx.plan(window);
const scene::CullPlan plan = idx.plan(window, grid.band);
parallel::ThreadPool::instance().parallel_for(
count, 1, [&](std::size_t lo, std::size_t hi) {
for (std::size_t s = lo; s < hi; ++s) {
scene::CullRegion cr{grid.brick_box(first + s).dilated(grid.band)};
scene::CullRegion cr{grid.brick_box(first + s).dilated(grid.band),
grid.band};
const scene::Tape brick = scene::compile_document(doc, &cr, &idx, &plan);
float* block = out + s * field::kBrickSamples;
bool near = false;
Expand Down
46 changes: 46 additions & 0 deletions include/clay/scene/bounds.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,52 @@ bool deformers_bound_gradient(const Node& item);
// is what meshing and raycast clipping want.
math::Aabb item_geometry_bound(const Node& item, const Layer& layer);

// HOW FAR PAST ITS BOUND A SQUASHED PLACEMENT'S FIELD STILL READS IN THE BAND,
// as the per-brick cull needs it (issue #649; tape.h, CullRegion).
//
// Outside its bound an item's field exceeds band + pad only when that field is
// a distance. Under a per-axis scale it is not: `cscale_nu_dist` multiplies the
// local value by the smallest component, so the field is short of the true
// distance by up to q = max(s) / min(s) at each level, the two ratios
// multiplied. A point D from the item's geometry therefore reads at least D / q
// (less the rounding), and the cull's "more than band + pad + w from the
// geometry" -- w being what the bound already dilates by, the rounding and the
// combine's support -- must become q times that. So the bound is widened by
//
// slope * (band + pad) + reach, slope = q - 1, reach = slope * w,
//
// before it is tested. A group carries the widest slope of its subtree and the
// widest reach, plus slope times its own blend support, for the same reason
// one level up.
//
// ZERO for a similarity -- the default (1, 1, 1) included -- exactly, since
// q is then max/min of three equal floats. So a document with no per-axis
// scale makes bit-identical cull decisions to the cull before this existed.
//
// It widens; it does not exempt. `item_geometry_reach_in_document` refuses a
// squashed placement outright because it has no band to widen by; the cull
// does, and keeps culling a squashed item everywhere its field cannot reach.
struct CullSquash {
float slope = 0.0f;
float reach = 0.0f;

bool none() const { return slope == 0.0f && reach == 0.0f; }
// The widening for a cull whose samples lie `band_pad` inside its test
// region: the caller's band plus the compiler's pad.
float dilation(float band_pad) const { return slope * band_pad + reach; }
void raise(const CullSquash& o) {
slope = kernel::cmax(slope, o.slope);
reach = kernel::cmax(reach, o.reach);
}
};

// One item, placed by its layer.
CullSquash item_cull_squash(const Node& item, const Layer& layer);

// A node as the cull tests it: an item as above, a group over its subtree (the
// box `node_influence_bound` returns, widened by this, is what it covers).
CullSquash node_cull_squash(const SdfContent& content, NodeId id, const Layer& layer);

// Whether an item's influence is confined to its own geometry. False means the
// item changes the field arbitrarily far away — a non-local op, an infinite
// grid repeat, or a primitive with no finite extent — and no finite bound may
Expand Down
29 changes: 26 additions & 3 deletions include/clay/scene/cull_index.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,24 @@ class CullIndex {
NodeId id;
math::Aabb bound;
bool local; // false: never culled (item_influence_is_local)
// How the cull widens `bound` for a squashed placement (issue #649):
// item_cull_squash for items, node_cull_squash for groups. Zero for
// every similarity, which is every node of most documents.
CullSquash squash;
};

// One coarse cull for a batch of compiles. `region` MUST contain every
// cull region the plan is later used with (dilate each by its band
// first, exactly as the per-brick CullRegion is built); the plan itself
// applies the feather pad, as the per-brick test does.
CullPlan plan(const math::Aabb& region) const;
//
// `band` is the widest CullRegion::band those regions carry. A squashed
// placement's widening grows with it, so the coarse cull widens each chain
// that holds one by its widest entry's widening at this band. A compile
// handed the plan with a wider band drops the plan and walks (see
// CullPlan::serves_band), so a caller that passes too little loses speed
// on a squashed document, never an item.
CullPlan plan(const math::Aabb& region, float band = 0.0f) const;

// -- extending an index after an append --------------------------------
//
Expand Down Expand Up @@ -142,8 +153,8 @@ class CullIndex {
// scan: an entry that can never be culled -- non-local, or an infinite
// bound -- is stored as an infinite box, so the three-clause survive
// test collapses to one box intersection; and 24 B a piece against the
// 40 B Entry lets the scan stream bounds rather than stride over the
// node pointers and ids it does not read.
// 48 B Entry (40 B when this was measured) lets the scan stream bounds
// rather than stride over the node pointers and ids it does not read.
//
// Both together are 8x on the scan itself at 50 000 items and 5.3x on
// `plan` around it, and the gap between those two numbers is the
Expand All @@ -160,6 +171,11 @@ class CullIndex {
// attacking.
std::vector<math::Aabb> probes;
bool prunable; // no feathered volume replace among the items
// The widest squash among the entries: the coarse scan widens its test
// region by this rather than each probe by its own, which keeps the
// packed scan one box test per entry and survival a superset of every
// per-brick decision. Zero for a chain with no squashed placement.
CullSquash widest;
};

// One layer's cull pad, kept as its TWO TERMS rather than as the sum, so
Expand Down Expand Up @@ -237,10 +253,17 @@ class CullPlan {
return it == pruned_.end() ? nullptr : &it->second;
}

// Whether a per-brick region carrying `band` may use this plan: always,
// unless a squashed placement was widened for a narrower band than that
// (CullIndex::plan).
bool serves_band(float band) const { return !squashed_ || band <= band_; }

private:
friend class CullIndex;
std::unordered_map<CullIndex::Key, std::vector<CullIndex::Entry>, CullIndex::KeyHash>
pruned_;
float band_ = 0.0f;
bool squashed_ = false;
};

// The cached-index form of CullIndex::append, and the ONE place the decision
Expand Down
26 changes: 26 additions & 0 deletions include/clay/scene/tape.h
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,34 @@ struct Tape {
// fixed dilation bounds it for an arbitrary document, which is why the
// clamp, not the fit, is the last word. Hard unions have no such term —
// min() is exact and associative — and measure identical at any length.
//
// A SQUASHED PLACEMENT reaches further than its bound for a third reason
// (issue #649). A per-axis scale — the item's or its layer's — makes the field
// a BOUND on the distance rather than the distance: `cscale_nu_dist` multiplies
// the local value by the smallest component, so the field can be short of the
// true distance by up to q = max(s) / min(s), the two levels' ratios
// multiplied. "More than band + pad from the bound, so more than band + pad in
// value" then holds only out to q times that, and a brick in between dropped an
// item whose field was inside the band there — in-band refill samples off the
// raw field by up to 0.068 on random documents. The compiler widens such an
// item's bound by (q - 1) * (band + pad + the dilation the bound already
// carries) before the test (scene::CullSquash): the item is still culled, just
// not short of where its field reaches.
//
// That is what `band` is for: how far inside `region` every sample the caller
// will trust lies — the dilation it applied to its brick. Zero claims the
// samples may sit anywhere in the region, which is right for a caller that names
// a region rather than a brick, and under-widens a squashed item for one that
// dilated a brick without saying so. Nothing else reads it: a document with no
// per-axis scale culls exactly as it did, whatever the band says.
struct CullRegion {
math::Aabb region;
float band = 0.0f;

CullRegion() = default;
// Not explicit: `CullRegion cull{box}` is how a caller that names a region
// rather than a brick spells it.
CullRegion(const math::Aabb& r, float b = 0.0f) : region(r), band(b) {}
};

class CullIndex;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
## Why
#649: a full brick build disagrees with the document's own raw field (`clay_eval_points`) at in-band samples. #652 fixed the deformer cull, which was the largest part. It named two mechanisms that remain, and this change fixes the second (B): **squashed placements are culled as if their field were a distance.**

A per-axis scale, on an item or on its layer, makes the field a bound on the distance rather than the distance. `cscale_nu_dist` multiplies the local value by the smallest component, so the value can be short of the true distance by up to `q = max(s) / min(s)`, and the two levels' ratios multiply. The per-brick cull drops an item once its bound is more than band + pad from the brick. For a squashed item that is not enough: its field there can still be inside the band out to q times that distance. Seeds 5743 and 6290 of the rich sweep reduce to this: 5,625 and 151 in-band samples off, worst 0.068.

`item_geometry_reach_in_document` already refuses a squashed placement for the same reason. The cull cannot refuse, because exempting every squashed item would make a squashed layer cull nothing. It widens the bound instead.

## What Changes
- `scene::CullRegion` gains `band`: how far inside the region the samples lie. Its zero means what every existing `CullRegion{box}` meant.
- `scene::CullSquash` (`item_cull_squash`, `node_cull_squash`): `slope = q - 1` and `reach = slope * w`, where w is the rounding and combine support the bound already carries. A group takes its subtree's widest slope and reach, plus slope times its own support. The cull tests the bound widened by `slope * (band + pad) + reach`.
- `CullIndex::Entry` caches the squash. `CullIndex::plan` takes the batch's widest band and widens each squashed chain's scan region. `CullPlan::serves_band` refuses a region with a wider band than the plan was made for, when the document holds a squashed placement.
- The band is set where it is known: the C ABI refill paths, the volume bake, gradient-normal meshing, and `clay_eval_grid`, which takes it from the lattice's margin inside the host's region.

## Impact
- A document with no per-axis scale makes bit-identical cull decisions. The squash is exactly zero for a similarity, and a test pins it.
- A squashed document keeps a squashed item in the bricks its field reaches, and no others.
- No ABI, format or version change. `CullIndex::Entry` grows from 40 to 48 bytes.
- Mechanism A of #649, the chain-pad envelope below 75 contributors, is not addressed. #649 stays open for it.
Loading
Loading