perf: stream propagation working sets

This commit is contained in:
2026-08-29 10:56:51 +09:00
parent de0053eae3
commit 044be3cf92
13 changed files with 1532 additions and 267 deletions
+267 -6
View File
@@ -11,6 +11,11 @@ const WALLS = [
];
const GEOMETRY_EPSILON = 1e-9;
export const DEFAULT_MAXIMUM_ACOUSTIC_PATH_EVALUATIONS = 2_000_000;
const trustedAcousticPathSummaries = new WeakSet();
const acousticPathSummaryCache = new WeakMap();
const acousticPathSummaryLru = new Map();
const MAXIMUM_ACOUSTIC_PATH_SUMMARY_CACHE_ENTRIES = 8;
function insideBounds(position, bounds, epsilon = GEOMETRY_EPSILON) {
return position.every(
@@ -223,10 +228,152 @@ export function buildAcousticPaths(options) {
return paths;
}
export function maximumAcousticDelaySeconds(options) {
const { scene, arrayDefinition, referenceDistance, minimumDistance } = options;
let maximum = 0;
for (const source of scene.sources) {
export function maximumAcousticPathsPerReceiver(scene) {
const acoustics = scene?.acoustics;
if (!acoustics || acoustics.model === "free-field") return 1;
let count = 1 + (Array.isArray(acoustics.occluders) ? acoustics.occluders.length : 0);
if (
acoustics.reflections?.order === 1 &&
acoustics.room
) {
count += 6;
if (acoustics.reflections.scattering > 0) count += 6;
}
if (acoustics.diffuseReverb?.enabled) {
count += acoustics.diffuseReverb.tapCount;
}
return count;
}
function acousticGeometrySignature(options) {
const {
scene,
arrayDefinition,
referenceDistance = 1,
minimumDistance = 0.01,
} = options;
return JSON.stringify({
version: 1,
speedOfSound: scene.speedOfSound,
seed: scene.seed ?? 0,
acoustics: scene.acoustics ?? null,
referenceDistance,
minimumDistance,
sources: scene.sources.map((source) => ({
id: source.id,
position: source.position,
})),
microphones: arrayDefinition.microphones.map((microphone) => ({
id: microphone.id,
position: microphone.position,
})),
});
}
function freezePathCounts(counts) {
return Object.freeze({ ...counts });
}
function createTrustedAcousticPathSummary(options) {
const {
signature,
sources,
microphoneCount,
} = options;
const pathCounts = {};
let pathCount = 0;
let maximumDelaySeconds = 0;
for (const source of sources) {
pathCount += source.pathCount;
maximumDelaySeconds = Math.max(maximumDelaySeconds, source.maximumDelaySeconds);
for (const [kind, count] of Object.entries(source.pathCounts)) {
pathCounts[kind] = (pathCounts[kind] ?? 0) + count;
}
}
const frozenSources = Object.freeze(sources.map((source) => Object.freeze({
sourceId: source.sourceId,
pathCount: source.pathCount,
pathCounts: freezePathCounts(source.pathCounts),
maximumDelaySeconds: source.maximumDelaySeconds,
})));
const summary = Object.freeze({
kind: "micarrsim.acoustic-path-summary/1",
signature,
sourceCount: frozenSources.length,
microphoneCount,
sourceReceiverPairCount: frozenSources.length * microphoneCount,
pathCount,
pathCounts: freezePathCounts(pathCounts),
maximumDelaySeconds,
sources: frozenSources,
});
trustedAcousticPathSummaries.add(summary);
return summary;
}
/**
* Enumerate sparse paths one source/receiver pair at a time and retain only
* numeric count/delay metadata. The returned immutable summary is internally
* branded so callers cannot forge a stale geometry cache.
*/
export function summarizeAcousticPaths(options) {
const {
scene,
arrayDefinition,
referenceDistance = 1,
minimumDistance = 0.01,
maximumPathEvaluations = DEFAULT_MAXIMUM_ACOUSTIC_PATH_EVALUATIONS,
} = options;
if (
!Number.isSafeInteger(maximumPathEvaluations) ||
maximumPathEvaluations <= 0
) {
throw new RangeError(
"maximumPathEvaluations must be a positive safe integer.",
);
}
const maximumPerReceiver = maximumAcousticPathsPerReceiver(scene);
const theoreticalPathEvaluations =
scene.sources.length * arrayDefinition.microphones.length * maximumPerReceiver;
if (
!Number.isSafeInteger(theoreticalPathEvaluations) ||
theoreticalPathEvaluations > maximumPathEvaluations
) {
throw new RangeError(
`acoustic path evaluation upper bound ${theoreticalPathEvaluations} exceeds ` +
`maximumPathEvaluations ${maximumPathEvaluations}.`,
);
}
const signature = acousticGeometrySignature({
scene,
arrayDefinition,
referenceDistance,
minimumDistance,
});
const lruSummary = acousticPathSummaryLru.get(signature);
if (lruSummary) {
acousticPathSummaryLru.delete(signature);
acousticPathSummaryLru.set(signature, lruSummary);
return lruSummary;
}
let arrayCache = acousticPathSummaryCache.get(scene);
if (!arrayCache) {
arrayCache = new WeakMap();
acousticPathSummaryCache.set(scene, arrayCache);
}
let parameterCache = arrayCache.get(arrayDefinition);
if (!parameterCache) {
parameterCache = new Map();
arrayCache.set(arrayDefinition, parameterCache);
}
const parameterKey = `${referenceDistance}\u0000${minimumDistance}`;
const cachedSummary = parameterCache.get(parameterKey);
if (cachedSummary?.signature === signature) return cachedSummary;
const sources = scene.sources.map((source) => {
const pathCounts = {};
let pathCount = 0;
let maximumDelaySeconds = 0;
for (const microphone of arrayDefinition.microphones) {
const paths = buildAcousticPaths({
scene,
@@ -237,8 +384,122 @@ export function maximumAcousticDelaySeconds(options) {
referenceDistance,
minimumDistance,
});
for (const path of paths) maximum = Math.max(maximum, path.delaySeconds);
for (const path of paths) {
if (!Number.isFinite(path.delaySeconds) || path.delaySeconds < 0) {
throw new RangeError("acoustic path delay must be finite and non-negative.");
}
pathCount += 1;
pathCounts[path.kind] = (pathCounts[path.kind] ?? 0) + 1;
maximumDelaySeconds = Math.max(maximumDelaySeconds, path.delaySeconds);
}
}
return {
sourceId: source.id,
pathCount,
pathCounts,
maximumDelaySeconds,
};
});
const summary = createTrustedAcousticPathSummary({
signature,
sources,
microphoneCount: arrayDefinition.microphones.length,
});
parameterCache.set(parameterKey, summary);
acousticPathSummaryLru.set(signature, summary);
while (
acousticPathSummaryLru.size >
MAXIMUM_ACOUSTIC_PATH_SUMMARY_CACHE_ENTRIES
) {
acousticPathSummaryLru.delete(acousticPathSummaryLru.keys().next().value);
}
return maximum;
return summary;
}
export function isAcousticPathSummaryCompatible(summary, options) {
return (
trustedAcousticPathSummaries.has(summary) &&
summary.signature === acousticGeometrySignature(options) &&
summary.sourceCount === options.scene.sources.length &&
summary.microphoneCount === options.arrayDefinition.microphones.length
);
}
/**
* Retarget an authenticated full-scene summary to the source subset retained
* by a quasi-static render window. Only immutable numeric metadata is shared.
*/
export function selectAcousticPathSummary(options) {
const {
summary,
sourceIndexes,
sourceScene,
scene,
arrayDefinition,
referenceDistance = 1,
minimumDistance = 0.01,
} = options;
if (!trustedAcousticPathSummaries.has(summary)) {
throw new TypeError("summary must be created by summarizeAcousticPaths.");
}
if (
!sourceScene ||
!isAcousticPathSummaryCompatible(summary, {
scene: sourceScene,
arrayDefinition,
referenceDistance,
minimumDistance,
})
) {
throw new RangeError(
"source acoustic path summary does not match its original geometry.",
);
}
if (
!Array.isArray(sourceIndexes) ||
sourceIndexes.length !== scene.sources.length
) {
throw new RangeError("sourceIndexes must correspond exactly to the selected scene sources.");
}
const sources = sourceIndexes.map((sourceIndex, selectedIndex) => {
const sourceSummary = summary.sources[sourceIndex];
if (
!sourceSummary ||
sourceSummary.sourceId !== scene.sources[selectedIndex].id
) {
throw new RangeError("selected acoustic path summary does not match scene source order.");
}
return sourceSummary;
});
const expectedSelectedSignature = acousticGeometrySignature({
scene: {
speedOfSound: sourceScene.speedOfSound,
seed: sourceScene.seed,
acoustics: sourceScene.acoustics,
sources: sourceIndexes.map((sourceIndex) => sourceScene.sources[sourceIndex]),
},
arrayDefinition,
referenceDistance,
minimumDistance,
});
const selectedSignature = acousticGeometrySignature({
scene,
arrayDefinition,
referenceDistance,
minimumDistance,
});
if (selectedSignature !== expectedSelectedSignature) {
throw new RangeError(
"selected scene path geometry does not match the summarized source geometry.",
);
}
return createTrustedAcousticPathSummary({
signature: selectedSignature,
sources,
microphoneCount: arrayDefinition.microphones.length,
});
}
export function maximumAcousticDelaySeconds(options) {
return summarizeAcousticPaths(options).maximumDelaySeconds;
}