perf: stream propagation working sets
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user