From 29e2cf14f4ab5faf9a9b3001764156e5285d5b4a Mon Sep 17 00:00:00 2001 From: Donovan Hutchence Date: Mon, 10 Aug 2026 17:24:58 +0200 Subject: [PATCH] latest --- README.md | 8 +++---- src/cli/index.ts | 24 ++++++++++---------- src/lib/decimate-uniform/README.md | 16 +++++++------- src/lib/decimate-uniform/gpu-knn.ts | 2 +- src/lib/decimate-uniform/index.ts | 12 ++++------ src/lib/decimate/decimate-source.ts | 2 +- src/lib/decimate/index.ts | 10 ++++++++- src/lib/decimate/priority.ts | 2 +- src/lib/index.ts | 16 +++++++------- src/lib/process.ts | 5 ++++- src/lib/workers/tasks.ts | 4 ++-- test/cli.test.mjs | 6 ++--- test/decimate-multiblock.test.mjs | 8 +++---- test/decimate-source.test.mjs | 22 +++++++++--------- test/decimate-uniform-parity.test.mjs | 2 +- test/decimate.test.mjs | 32 +++++++++++++++++---------- tools/decimate-parity.mjs | 15 +++++++------ 17 files changed, 101 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index 739dbb34..5bdd17b9 100644 --- a/README.md +++ b/README.md @@ -118,10 +118,10 @@ Actions execute in the order specified and can be repeated. Any action may appea opacity, scale_*, f_dc_* use transformed values (linear opacity 0-1, linear scale, linear color 0-1). Append _raw for raw PLY values (e.g. opacity_raw). --d, --decimate Simplify, allocating removal by local error (adaptive) - Use n% for a percentage. --decimate-uniform removes at a uniform - rate instead (the pre-3.2 algorithm): lower memory, and better on - uniformly-sized Gaussians. +-d, --decimate Simplify at a uniform rate everywhere + Use n% for a percentage. --decimate-adaptive allocates removal + by local error instead: much better on mixed-scale content such + as skies, at higher memory cost. Memory-bounded and streaming: scales to scenes of 100M+ Gaussians. Must be the final action, and the output must be .ply (write a decimated PLY first, then convert in a diff --git a/src/cli/index.ts b/src/cli/index.ts index 7658f28a..ec53f6cf 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -16,7 +16,7 @@ import { DataTable, dataTableToChunkSource, decimateSource, - decimateSourceUniform, + decimateSourceAdaptive, fmtBytes, fmtCount, fmtTime, @@ -127,12 +127,12 @@ const resolveInput = (arg: string): ResolvedInput => { // never dispatched as a data operation). type CliAction = ProcessAction | { kind: 'lod'; value: number }; -// `--decimate` and `--decimate-uniform` both produce a decimate action, so +// `--decimate` and `--decimate-adaptive` both produce a decimate action, so // which decimator to run rides on the action itself rather than on global // options — that way it always describes the action actually executed, with no // dependence on flag ordering or on the "exactly one decimate action" check. // The extra field is stripped before actions reach the library. -type CliDecimate = Extract & { uniform: boolean }; +type CliDecimate = Extract & { adaptive: boolean }; // Strip the CLI-only lod tags, narrowing back to dispatchable actions. const stripLodTags = (actions: CliAction[]): ProcessAction[] => { @@ -199,7 +199,7 @@ const cliOptionsConfig = { 'filter-box': { type: 'string', short: 'B', multiple: true }, 'filter-sphere': { type: 'string', short: 'S', multiple: true }, 'decimate': { type: 'string', short: 'd', multiple: true }, - 'decimate-uniform': { type: 'string', multiple: true }, + 'decimate-adaptive': { type: 'string', multiple: true }, 'filter-cluster': { type: 'string', short: 'C', multiple: true }, 'filter-floaters': { type: 'string', short: 'F', multiple: true }, params: { type: 'string', short: 'p', multiple: true }, @@ -704,7 +704,7 @@ const parseArguments = async () => { }); break; case 'decimate': - case 'decimate-uniform': { + case 'decimate-adaptive': { const value = t.value.trim(); let count: number | null = null; let percent: number | null = null; @@ -727,7 +727,7 @@ const parseArguments = async () => { kind: 'decimate', count, percent, - uniform: t.name === 'decimate-uniform' + adaptive: t.name === 'decimate-adaptive' }; current.processActions.push(decimate); break; @@ -806,12 +806,12 @@ ACTIONS (executed in order; can be repeated) -S, --filter-sphere Remove Gaussians outside sphere -V, --filter-value Keep Gaussians where ; cmp ∈ {lt,lte,gt,gte,eq,neq} - -d, --decimate Simplify, allocating removal by local error (adaptive; default). - Much better on mixed-scale content such as skies. - --decimate-uniform Simplify at a uniform rate everywhere (the pre-3.2 algorithm). + -d, --decimate Simplify at a uniform rate everywhere (default). Lower memory, and better at depth on uniformly-sized Gaussians: uniform texture, single objects, snow. - Must be the final action, and the output must be .ply + --decimate-adaptive Simplify, allocating removal by local error (adaptive). + Much better on mixed-scale content such as skies. + Either must be the final action, with a .ply output --scratch-dir Directory for decimation spill files (deep targets on huge scenes). Default: the output file's directory -F, --filter-floaters [size,op,min] Remove Gaussians not contributing to any solid voxel. Default: 0.05,0.1,0.004 @@ -1279,8 +1279,8 @@ const main = async () => { scratchDir: options.scratchDir ?? dirname(outputFilename), remove: (path: string) => unlink(path) }; - combined = decimateAction.uniform ? - await decimateSourceUniform(combined, pool, { + combined = decimateAction.adaptive ? + await decimateSourceAdaptive(combined, pool, { targetCount: keepCount, createDevice: deviceCreator, memoryBudgetBytes: options.memoryBudgetBytes, diff --git a/src/lib/decimate-uniform/README.md b/src/lib/decimate-uniform/README.md index f671cc3b..b14235bb 100644 --- a/src/lib/decimate-uniform/README.md +++ b/src/lib/decimate-uniform/README.md @@ -1,8 +1,8 @@ # decimate-uniform — the pre-3.2 decimator -The decimator that shipped up to 3.1.x, reached through `--decimate-uniform` / -`decimateSourceUniform()`. `src/lib/decimate/` holds the adaptive one -(`--decimate` / `decimateSource()`). +The decimator that shipped up to 3.1.x and the default again, reached through +`--decimate` / `decimateSource()`. `src/lib/decimate/` holds the adaptive one +(`--decimate-adaptive` / `decimateSourceAdaptive()`). The names describe how each allocates removal, not a ranking. Both are supported and neither is a fallback for the other: they win on different @@ -65,8 +65,8 @@ to be deliberate rather than incidental. Before landing one: - Re-run the whole-scene comparison if you expect output to be unchanged. That is what `tools/decimate-parity.mjs` is for — it chains halvings through a - reference binary's `--decimate` and this tree's `--decimate-uniform`, compares - byte for byte, reports PSNR for both, and exits non-zero on any mismatch: + reference binary's `--decimate` and this tree's `--decimate`, compares byte + for byte, reports PSNR for both, and exits non-zero on any mismatch: ```bash node tools/decimate-parity.mjs sky --ref splat-transform @@ -85,7 +85,7 @@ to be deliberate rather than incidental. Before landing one: ## If it is ever retired -Nothing in `src/lib/decimate/` refers to this directory, so: `rm -rf` it, drop -`decimateSourceUniform` from `src/lib/index.ts`, drop `--decimate-uniform` -from the CLI, and delete `test/decimate-uniform-parity.test.mjs` and +Nothing in `src/lib/decimate/` refers to this directory, so: `rm -rf` it, point +`decimateSource` in `src/lib/index.ts` and `--decimate` in the CLI back at the +adaptive path, and delete `test/decimate-uniform-parity.test.mjs` and `tools/decimate-parity.mjs`. diff --git a/src/lib/decimate-uniform/gpu-knn.ts b/src/lib/decimate-uniform/gpu-knn.ts index 11f49881..c4f3248d 100644 --- a/src/lib/decimate-uniform/gpu-knn.ts +++ b/src/lib/decimate-uniform/gpu-knn.ts @@ -18,7 +18,7 @@ import { import { type FlatKdTree } from '../spatial/kd-tree'; /** - * Block-local GPU KNN for `--decimate-uniform`. + * Block-local GPU KNN for `--decimate`. * * Separate from the quality path's `gpu/gpu-knn.ts` because the two have * incompatible models: this one takes a max size and accepts a DIFFERENT tree diff --git a/src/lib/decimate-uniform/index.ts b/src/lib/decimate-uniform/index.ts index 017a2a64..f3709e7b 100644 --- a/src/lib/decimate-uniform/index.ts +++ b/src/lib/decimate-uniform/index.ts @@ -1,8 +1,4 @@ -// The pre-3.2 decimator — see README.md. Its `decimateSource` is exported -// under the `Uniform` name so the two paths can coexist without renaming -// anything inside this directory. -export { - decimateSource as decimateSourceUniform, - type DecimateOptions as DecimateUniformOptions, - type DecimateSpill as DecimateUniformSpill -} from './decimate-source'; +// The pre-3.2 decimator, reached through `--decimate` / `decimateSource()` — +// see README.md. The adaptive path (`--decimate-adaptive`) is exported under +// the `Adaptive` name from ../decimate/. +export { decimateSource, type DecimateOptions, type DecimateSpill } from './decimate-source'; diff --git a/src/lib/decimate/decimate-source.ts b/src/lib/decimate/decimate-source.ts index ed9603fa..b29b0999 100644 --- a/src/lib/decimate/decimate-source.ts +++ b/src/lib/decimate/decimate-source.ts @@ -318,7 +318,7 @@ const decimateSource = async ( if (!device) { throw new Error( `multi-block adaptive decimation requires WebGPU (${fmtCount(N)} splats, ` + - `${fmtCount(blockSize)}-splat cores); provide a device, or use --decimate-uniform` + `${fmtCount(blockSize)}-splat cores); provide a device, or use --decimate` ); } if (!opts.spill) { diff --git a/src/lib/decimate/index.ts b/src/lib/decimate/index.ts index 246fe701..16cfcd05 100644 --- a/src/lib/decimate/index.ts +++ b/src/lib/decimate/index.ts @@ -1,3 +1,11 @@ -export { decimateSource, type DecimateOptions, type DecimateSpill } from './decimate-source'; +// The adaptive decimator (`--decimate-adaptive`). Its `decimateSource` is +// exported under the `Adaptive` name so the two paths can coexist without +// renaming anything inside this directory; `decimateSource` is the pre-3.2 +// path in ../decimate-uniform/. +export { + decimateSource as decimateSourceAdaptive, + type DecimateOptions as DecimateAdaptiveOptions, + type DecimateSpill as DecimateAdaptiveSpill +} from './decimate-source'; export { mergeGroup, createMergeScratch, splatMass, makeGaussianSamples, type SplatView, type MergedOut, type MergeScratch } from './moment-match'; export { kdPartition, coherenceRuns, type BlockRange, type ResidentPositions } from './partition'; diff --git a/src/lib/decimate/priority.ts b/src/lib/decimate/priority.ts index fcd9be2e..91eb174b 100644 --- a/src/lib/decimate/priority.ts +++ b/src/lib/decimate/priority.ts @@ -35,7 +35,7 @@ type CandidateArrays = { /** * Colour components the pass gathers per row. The field-L2 cost reads only * DC, so at SH band 3 this is 16× less colour RAM and copy traffic per block - * than gathering every band. (`--decimate-uniform` scores full SH and has its + * than gathering every band. (`--decimate` scores full SH and has its * own pass — see lib/decimate-uniform/priority.ts.) */ const COLOR_COMPONENTS = 3; diff --git a/src/lib/index.ts b/src/lib/index.ts index d51a1669..f1f32c0f 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -43,14 +43,14 @@ export type { Decimate } from './process'; -// Chunk-native decimation -export { decimateSource } from './decimate'; -export type { DecimateOptions, DecimateSpill } from './decimate'; - -// The frozen pre-3.2 decimator (--decimate-uniform); see -// lib/decimate-uniform/README.md -export { decimateSourceUniform } from './decimate-uniform'; -export type { DecimateUniformOptions, DecimateUniformSpill } from './decimate-uniform'; +// Chunk-native decimation (--decimate): the frozen pre-3.2 decimator, which +// removes at a uniform rate everywhere; see lib/decimate-uniform/README.md +export { decimateSource } from './decimate-uniform'; +export type { DecimateOptions, DecimateSpill } from './decimate-uniform'; + +// The adaptive decimator (--decimate-adaptive), allocating removal by local error +export { decimateSourceAdaptive } from './decimate'; +export type { DecimateAdaptiveOptions, DecimateAdaptiveSpill } from './decimate'; // Statistics export { computeStats } from './stats'; diff --git a/src/lib/process.ts b/src/lib/process.ts index 1c25c63b..b076917d 100644 --- a/src/lib/process.ts +++ b/src/lib/process.ts @@ -3,7 +3,7 @@ import { Vec3 } from 'playcanvas'; import { createChunkDataPool } from './chunk'; import { dataTableToChunkSource, materializeToDataTable } from './compat/data-table'; import { Column, DataTable, sortMortonOrder, convertToSpace, getSHBands } from './data-table'; -import { decimateSource } from './decimate'; +import { decimateSource } from './decimate-uniform'; import { computeSourceStats } from './ops'; import { type InputFormat } from './read'; import { formatSourceInfo, formatSourceStats } from './source-info'; @@ -158,6 +158,9 @@ type MortonOrder = { * Instead of discarding low-visibility splats, this iteratively merges nearby * similar splats into single approximating Gaussians using Mass-Preserving * Moment Matching (MPMM), preserving scene structure and appearance. + * + * Removal is allocated at a uniform rate everywhere; for the adaptive variant + * call `decimateSourceAdaptive()` directly. */ type Decimate = { /** Action type identifier. */ diff --git a/src/lib/workers/tasks.ts b/src/lib/workers/tasks.ts index 61408e21..0195e536 100644 --- a/src/lib/workers/tasks.ts +++ b/src/lib/workers/tasks.ts @@ -103,7 +103,7 @@ const taskHandlers = { }, // Build + flatten a KD-tree over interleaved LOCAL positions (the - // `--decimate-uniform` GPU path: node splat ids stay local and the + // `--decimate` GPU path: node splat ids stay local and the // flattened arrays upload straight into that path's GpuKnn). Serves // decimate-uniform/ — see its README before changing either handler. flattenKdTree: (args: { positions: Float32Array }): TaskOutput => { @@ -125,7 +125,7 @@ const taskHandlers = { }; }, - // `--decimate-uniform` CPU-fallback block KNN: exact k-NN of the owned + // `--decimate` CPU-fallback block KNN: exact k-NN of the owned // prefix within the local point set, as local indices. Frozen. knnBlock: (args: { positions: Float32Array, ownedCount: number, k: number }): TaskOutput => { const result = knnQueryBlock(args.positions, args.ownedCount, args.k); diff --git a/test/cli.test.mjs b/test/cli.test.mjs index a1781970..13852ffc 100644 --- a/test/cli.test.mjs +++ b/test/cli.test.mjs @@ -185,17 +185,17 @@ describe('CLI decimate (terminal PLY restriction)', () => { assert.strictEqual(written, Math.round(inputCount / 2), `50% of ${inputCount}`); }); - it('--decimate-uniform runs the pre-3.2 algorithm', async () => { + it('--decimate-adaptive runs the adaptive algorithm', async () => { const { mkdtemp, readFile: readFileFs, rm } = await import('node:fs/promises'); const { tmpdir } = await import('node:os'); const { join } = await import('node:path'); - const dir = await mkdtemp(join(tmpdir(), 'st-decimate-uniform-cli-')); + const dir = await mkdtemp(join(tmpdir(), 'st-decimate-adaptive-cli-')); const balancedPath = join(dir, 'balanced.ply'); const balanced = await runCli([ '--gpu', 'cpu', 'test/fixtures/splat/minimal.splat', - '--decimate-uniform', '50%', + '--decimate-adaptive', '50%', balancedPath ]); assert.strictEqual(balanced.code, 0, `balanced CLI failed:\n${balanced.stderr}\n${balanced.stdout}`); diff --git a/test/decimate-multiblock.test.mjs b/test/decimate-multiblock.test.mjs index b0368488..e0a00bfb 100644 --- a/test/decimate-multiblock.test.mjs +++ b/test/decimate-multiblock.test.mjs @@ -3,7 +3,7 @@ import { after, before, describe, it } from 'node:test'; import { makeSyntheticSource } from './helpers/synthetic-source.mjs'; -import { decimateSource } from '../src/lib/decimate/index.js'; +import { decimateSourceAdaptive } from '../src/lib/decimate/index.js'; import { MemoryReadSource } from '../src/lib/io/read/memory-file-system.js'; import { MemoryFileSystem } from '../src/lib/io/write/memory-file-system.js'; @@ -22,11 +22,11 @@ after(() => { device?.destroy?.(); }); -describe('decimateSource multi-block adaptive path', () => { +describe('decimateSourceAdaptive multi-block adaptive path', () => { it('fails clearly when the memory budget requires multiple blocks without WebGPU', async () => { const { source, pool } = await makeSyntheticSource(65540, 0, 123, { chunkSize: 1024 }); await assert.rejects( - decimateSource(source, pool, { targetCount: 65000, memoryBudgetBytes: 1 }), + decimateSourceAdaptive(source, pool, { targetCount: 65000, memoryBudgetBytes: 1 }), /multi-block adaptive decimation requires WebGPU/ ); }); @@ -55,7 +55,7 @@ describe('decimateSource multi-block adaptive path', () => { } }; - const out = await decimateSource(source, pool, { + const out = await decimateSourceAdaptive(source, pool, { targetCount, createDevice: async () => device, memoryBudgetBytes: 1, diff --git a/test/decimate-source.test.mjs b/test/decimate-source.test.mjs index b4c1d3db..1165466f 100644 --- a/test/decimate-source.test.mjs +++ b/test/decimate-source.test.mjs @@ -1,5 +1,5 @@ /** - * decimateSource orchestrator tests: exact counts, value domains, deep + * decimateSourceAdaptive orchestrator tests: exact counts, value domains, deep * targets (multi-generation with RAM intermediates), spill path with temp * cleanup, in-domain aggregate statistics, and input validation. */ @@ -12,7 +12,7 @@ import { describe, it } from 'node:test'; import { makeSyntheticSource } from './helpers/synthetic-source.mjs'; -import { decimateSource } from '../src/lib/decimate/index.js'; +import { decimateSourceAdaptive } from '../src/lib/decimate/index.js'; // Read the requested layers in ONE sequential pass (the producer contract: // each chunk is served exactly once, all layers together). @@ -61,11 +61,11 @@ const stats = (geo, n) => { }; }; -describe('decimateSource', () => { +describe('decimateSourceAdaptive', () => { it('50% decimation: exact count, single generation, values in-domain', async () => { const n = 4000; const { source, pool } = await makeSyntheticSource(n, 1, 17, { chunkSize: 512 }); - const out = await decimateSource(source, pool, { targetCount: 2000 }); + const out = await decimateSourceAdaptive(source, pool, { targetCount: 2000 }); assert.strictEqual(out.meta.numGaussians, 2000); const { position: pos, geometric: geo } = await readLayers(out, pool, ["position", "geometric"]); await out.close(); @@ -81,7 +81,7 @@ describe('decimateSource', () => { it('deep target runs multiple generations (RAM intermediates) to the exact count', async () => { const n = 4000; const { source, pool } = await makeSyntheticSource(n, 0, 19, { chunkSize: 512 }); - const out = await decimateSource(source, pool, { targetCount: 500 }); // 12.5% → 3 generations + const out = await decimateSourceAdaptive(source, pool, { targetCount: 500 }); // 12.5% → 3 generations assert.strictEqual(out.meta.numGaussians, 500); const { geometric: geo } = await readLayers(out, pool, ["geometric"]); await out.close(); @@ -91,7 +91,7 @@ describe('decimateSource', () => { it('decimated output has finite, in-domain aggregate statistics', async () => { const n = 3000; const { source, pool } = await makeSyntheticSource(n, 1, 23, { chunkSize: 512 }); - const out = await decimateSource(source, pool, { targetCount: 1500 }); + const out = await decimateSourceAdaptive(source, pool, { targetCount: 1500 }); const { geometric: oursGeo } = await readLayers(out, pool, ["geometric"]); await out.close(); @@ -107,7 +107,7 @@ describe('decimateSource', () => { const scratchDir = await mkdtemp(join(tmpdir(), 'decimate-spill-')); const { NodeFileSystem, NodeReadFileSystem } = await import('../src/cli/node-file-system.js'); let sawSpill = false; - const out = await decimateSource(source, pool, { + const out = await decimateSourceAdaptive(source, pool, { targetCount: 400, // deep target → intermediates memoryBudgetBytes: 1, // force the spill path spill: { @@ -136,7 +136,7 @@ describe('decimateSource', () => { chunkSize: 256, extraColumns: [{ name: 'tag', type: 'uint32' }] }); - const out = await decimateSource(source, pool, { targetCount: 600 }); + const out = await decimateSourceAdaptive(source, pool, { targetCount: 600 }); assert.deepStrictEqual([...out.meta.extraColumns.map(e => e.name)], ['tag']); const { other } = await readLayers(out, pool, ["other"]); await out.close(); @@ -145,10 +145,10 @@ describe('decimateSource', () => { it('target >= N passes the source through; invalid inputs throw', async () => { const { source, pool } = await makeSyntheticSource(100, 0, 41, { chunkSize: 64 }); - const out = await decimateSource(source, pool, { targetCount: 100 }); + const out = await decimateSourceAdaptive(source, pool, { targetCount: 100 }); assert.strictEqual(out, source); - await assert.rejects(() => decimateSource(source, pool, { targetCount: 0 }), /at least 1/); + await assert.rejects(() => decimateSourceAdaptive(source, pool, { targetCount: 0 }), /at least 1/); // multi-LOD source rejected const { buffers } = await makeSyntheticSource(50, 0, 43, { chunkSize: 1024 }); @@ -165,7 +165,7 @@ describe('decimateSource', () => { geometric: [buffers.geometric[0], slice(buffers.geometric, 25, 32)], color: [buffers.color[0], slice(buffers.color, 25, 12)] }); - await assert.rejects(() => decimateSource(multiLod, pool, { targetCount: 10 }), /single-LOD/); + await assert.rejects(() => decimateSourceAdaptive(multiLod, pool, { targetCount: 10 }), /single-LOD/); await multiLod.close(); }); }); diff --git a/test/decimate-uniform-parity.test.mjs b/test/decimate-uniform-parity.test.mjs index a78fd85f..90c3d924 100644 --- a/test/decimate-uniform-parity.test.mjs +++ b/test/decimate-uniform-parity.test.mjs @@ -1,5 +1,5 @@ /** - * Output-parity guards for the `--decimate-uniform` decimator in + * Output-parity guards for the `--decimate` decimator in * `src/lib/decimate-uniform/` — see that directory's README. * * That path is bit-for-bit output-compatible with the 3.1.6 binary, which is diff --git a/test/decimate.test.mjs b/test/decimate.test.mjs index 5e5f0c80..4cdba6d9 100644 --- a/test/decimate.test.mjs +++ b/test/decimate.test.mjs @@ -16,7 +16,10 @@ import { Vec3 } from 'playcanvas'; import { assertClose } from './helpers/summary-compare.mjs'; import { createMinimalTestData } from './helpers/test-utils.mjs'; -import { Column, DataTable, processDataTable } from '../src/lib/index.js'; +import { + Column, DataTable, createChunkDataPool, dataTableToChunkSource, + decimateSourceAdaptive, processDataTable +} from '../src/lib/index.js'; function createGaussianTestData(options = {}) { const count = options.count ?? 4; @@ -203,19 +206,24 @@ describe('decimate - merge quality invariants', () => { ); }); - it('should decimate a fully-coincident scene (re-costed selection)', async () => { - // Every splat coincident at the origin: the legacy one-shot matching - // starved on the collapsed KNN hub set and failed loud. Re-costed + it('should fail loud on a fully-coincident scene, where the adaptive path merges', async () => { + // Every splat coincident at the origin. The action runs the uniform + // decimator (--decimate), whose one-shot matching starves on the + // collapsed KNN hub set and fails loud rather than grinding. Re-costed // selection derives candidates from the full neighbour graph as - // clusters form, so coincident scenes now merge cleanly (coincident - // merges are exactly lossless under the field-L2 cost). + // clusters form, so --decimate-adaptive merges these cleanly instead + // (coincident merges are exactly lossless under the field-L2 cost). const testData = createGaussianTestData({ count: 600 }); - const result = await processDataTable(testData, decimate(300)); - assert.strictEqual(result.numRows, 300); - const op = result.getColumnByName('opacity').data; - for (let i = 0; i < 300; i++) { - assert.ok(Number.isFinite(op[i]), `row ${i} opacity finite`); - } + await assert.rejects( + () => processDataTable(testData, decimate(300)), + /decimation stalled/, + 'uniform path refuses to grind on a degenerate neighbour graph' + ); + + const pool = createChunkDataPool(); + const out = await decimateSourceAdaptive(dataTableToChunkSource(testData), pool, { targetCount: 300 }); + assert.strictEqual(out.meta.numGaussians, 300); + await out.close(); }); it('should throw when gaussian columns are missing (legacy silently pruned)', async () => { diff --git a/tools/decimate-parity.mjs b/tools/decimate-parity.mjs index 5cc445f6..a35e8c62 100644 --- a/tools/decimate-parity.mjs +++ b/tools/decimate-parity.mjs @@ -1,21 +1,22 @@ #!/usr/bin/env node /** - * Output-parity check for `--decimate-uniform` against a reference build. + * Output-parity check for `--decimate` against a reference build. * * The uniform decimator is bit-for-bit output-compatible with the 3.1.x * release, which is what makes it a usable reference baseline (see * src/lib/decimate-uniform/README.md and the `old` column in * scenes/DECIMATION-RESULTS.md). This script is how that claim is checked: * run N chained halvings through a reference binary's `--decimate` and this - * working tree's `--decimate-uniform`, compare the outputs byte for byte, and - * report PSNR for both against the undecimated source. + * working tree's `--decimate`, compare the outputs byte for byte, and report + * PSNR for both against the undecimated source. * * Exits non-zero if any level differs, so it can gate a change. * * Prerequisites: * - npm run build (imports WebPCodec from ../dist) - * - a reference binary on PATH, or --ref . The reference must predate - * the split, i.e. its `--decimate` IS the uniform algorithm. + * - a reference binary on PATH, or --ref . It is invoked with + * `--decimate`, so that flag must be the uniform algorithm there: any + * 3.1.x build (3.2.x spelled it `--decimate-uniform`). * * Usage: * node tools/decimate-parity.mjs [sky|snow] [options] @@ -114,11 +115,11 @@ const chain = (label, argsFor) => { return { paths, secs }; }; -console.log(`\n=== ${which}: ${ref} --decimate vs this tree --decimate-uniform ===`); +console.log(`\n=== ${which}: ${ref} --decimate vs this tree --decimate ===`); console.log(`source ${source} (${vertexCount(source)} splats), ${halvings} halvings\n`); const a = chain('ref', { bin: [ref], flags: ['--decimate'] }); -const b = chain('uni', { bin: [NODE, CLI], flags: ['--decimate-uniform'] }); +const b = chain('uni', { bin: [NODE, CLI], flags: ['--decimate'] }); let mismatches = 0; console.log('level count ref s uni s identical');