/*
* Copyright 2026 The Ray Optics Simulation authors and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
collectNodeLabels,
collectParameterNames
} from '../../formula/dag-util.js';
import { estimateDagRanges } from '../../formula/range-estimator.js';
import {
createDagWgslSpecialization
} from '../../formula/dag-wgsl-generator.js';
import { getIntersectionTolerancePolicy } from '../../primitive/numeric.js';
export const WEBGPU_F32_MAX = 3.4028234663852886e38;
export const WEBGPU_F32_MIN_NORMAL = 2 ** -126;
export const WEBGPU_MAX_POLARIZED_POWER = WEBGPU_F32_MAX / 2;
export const DEFAULT_WEBGPU_PARAMETER_INTERVAL_LIMIT = 8;
// With every world coordinate in [-2^62, 2^62], subtracting two coordinates
// is finite and the sum of the squared x/y differences is at most 2^127.
// This leaves a factor-of-two margin below the largest f32 value for the
// distance and normal calculations shared with the primitive CPU engine.
// Scale-normalized curve algorithms can have additional constraints; those
// belong to WebGPU geometry packing rather than to the world-coordinate range.
export const WEBGPU_SPATIAL_MAX = 2 ** 62;
const DEFAULT_VIOLET_WAVELENGTH = 420;
const DEFAULT_RED_WAVELENGTH = 620;
const BASE_UV_WAVELENGTH = 380;
const BASE_INFRARED_WAVELENGTH = 700;
const SOURCE_RESERVED_PARAMETERS = ['i', 'N'];
const BULK_RESERVED_PARAMETERS = ['x', 'y', 'lambda'];
const DETECTOR_RESERVED_PARAMETERS = [
'd_0x',
'd_0y',
'P_0s',
'P_0p',
'lambda',
'x',
'y',
'u',
'sigma'
];
const SURFACE_RESERVED_PARAMETERS = [
...DETECTOR_RESERVED_PARAMETERS,
'n_0',
'n_1'
];
const INTERNAL_BOUNDARY_PARAMETERS = [
'd_0x',
'd_0y',
'P_0s',
'P_0p',
'n_0',
'n_1'
];
const F32 = new Float32Array(1);
const U32 = new Uint32Array(F32.buffer);
const MULTIPLY_RANGE_DAG = {
root: 2,
nodes: [
{ id: 0, kind: 'parameter', name: 'left' },
{ id: 1, kind: 'parameter', name: 'right' },
{ id: 2, kind: 'binary', op: '*', args: [0, 1] }
]
};
/**
* Estimate the f32 input interval contract for every formula type in a
* processed scene. Each authored type receives a reusable WGSL specialization
* containing its DAG range result and range-dependent guard signature. A
* later WGSL generation step can consume that specialization without running
* the range estimator again. No WGSL is emitted or compiled here.
*
* Runtime WebGPU tracing must discard wavelengths outside `wavelengthRange`.
* The same range is used for every downstream `lambda` input.
*
* @param {Object} description - Engine-independent processed scene.
* @param {Object} [options]
* @param {number} [options.violetWavelength=420]
* @param {number} [options.redWavelength=620]
* @param {boolean} [options.keepNonVisibleLight=false]
* @param {number} [options.intervalLimit=8]
* @returns {Object} A unified `types` list, category views, and supporting region-index metadata.
*/
export function estimateWebGpuParameterRanges(description, {
violetWavelength = DEFAULT_VIOLET_WAVELENGTH,
redWavelength = DEFAULT_RED_WAVELENGTH,
keepNonVisibleLight = false,
intervalLimit = DEFAULT_WEBGPU_PARAMETER_INTERVAL_LIMIT
} = {}) {
validateDescription(description);
validateIntervalLimit(intervalLimit);
validateWebGpuSpatialBounds(description);
const wavelengthRange = deriveWebGpuWavelengthRange({
violetWavelength,
redWavelength,
keepNonVisibleLight
});
const ownerRanges = collectOwnerCoordinateRanges(description, intervalLimit);
const tolerancePolicy = getIntersectionTolerancePolicy(
description.numericEpsilon
);
const regionRefractiveIndices = estimateRegionRefractiveIndices(
description,
ownerRanges.regionBounds,
wavelengthRange,
intervalLimit
);
const bulkRefractiveIndices = description.types.bulks.map((_, typeId) =>
unionRangeInfos(
regionRefractiveIndices.filter(info => info.bulkTypeId === typeId),
intervalLimit
)
);
const sourceTypes = description.types.sources.map((type, typeId) =>
createTypeRangeEntry({
type,
typeId,
instances: description.sources.filter(
source => source.sourceTypeId === typeId
),
reservedNames: SOURCE_RESERVED_PARAMETERS,
reservedRanges: createSourceReservedRanges(
description.sources.filter(source => source.sourceTypeId === typeId),
intervalLimit
),
intervalLimit,
categoryName: 'source'
})
);
const surfaceTypes = description.types.surfaces.map((type, typeId) => {
const instances = description.surfaces.filter(
surface => surface.surfaceTypeId === typeId
);
const curves = description.curves.filter(curve =>
curve.ownerKind === 'surface' &&
description.surfaces[curve.ownerId]?.surfaceTypeId === typeId
);
const refractiveIndexRange = interactionRefractiveIndexRange(
curves,
regionRefractiveIndices,
ownerRanges.regionBounds,
intervalLimit
);
return createTypeRangeEntry({
type,
typeId,
instances,
reservedNames: SURFACE_RESERVED_PARAMETERS,
reservedRanges: createInteractionReservedRanges({
coordinateRanges: ownerRanges.surfaces[typeId],
curves,
wavelengthRange,
refractiveIndexRange,
tolerancePolicy,
intervalLimit
}),
intervalLimit,
categoryName: 'surface'
});
});
const bulkTypes = description.types.bulks.map((type, typeId) => {
const instances = description.regions.filter(
region => region.bulkTypeId === typeId
);
return {
...createTypeRangeEntry({
type,
typeId,
instances,
reservedNames: BULK_RESERVED_PARAMETERS,
reservedRanges: {
...(ownerRanges.bulks[typeId] ?? zeroCoordinateRanges()),
lambda: wavelengthRange
},
intervalLimit,
categoryName: 'bulk'
}),
outputRanges: {
n: cloneRangeInfo(bulkRefractiveIndices[typeId])
}
};
});
const detectorTypes = description.types.detectors.map((type, typeId) => {
const instances = description.detectors.filter(
detector => detector.detectorTypeId === typeId
);
const curves = description.curves.filter(curve =>
curve.ownerKind === 'detector' &&
description.detectors[curve.ownerId]?.detectorTypeId === typeId
);
return createTypeRangeEntry({
type,
typeId,
instances,
reservedNames: DETECTOR_RESERVED_PARAMETERS,
reservedRanges: createInteractionReservedRanges({
coordinateRanges: ownerRanges.detectors[typeId],
curves,
wavelengthRange,
tolerancePolicy,
intervalLimit
}),
intervalLimit,
categoryName: 'detector'
});
});
const internalSurfaceTypes = [false, true].flatMap(partialReflect => {
const regions = description.regions.filter(
region => region.partialReflect === partialReflect
);
if (regions.length === 0) return [];
const curves = description.curves.filter(curve =>
curve.ownerKind === 'region' &&
description.regions[curve.ownerId]?.partialReflect === partialReflect
);
const refractiveIndexRange = interactionRefractiveIndexRange(
curves,
regionRefractiveIndices,
ownerRanges.regionBounds,
intervalLimit
);
return [createInternalBoundaryType(
partialReflect,
refractiveIndexRange
)];
});
for (const type of sourceTypes) {
attachDagSpecialization(
type,
description.types.sources[type.typeId].definition.dag
);
validateSourceCoordinateOutputs(
type,
description.types.sources[type.typeId].definition.dag
);
}
for (const type of bulkTypes) {
attachDagSpecialization(
type,
description.types.bulks[type.typeId].definition.dag
);
}
for (const type of surfaceTypes) {
attachDagSpecialization(
type,
description.types.surfaces[type.typeId].definition.dag
);
}
for (const type of detectorTypes) {
attachDagSpecialization(
type,
description.types.detectors[type.typeId].definition.dag
);
}
const types = [
...sourceTypes,
...bulkTypes,
...internalSurfaceTypes,
...surfaceTypes,
...detectorTypes
];
return {
wavelengthRange,
types,
sources: sourceTypes,
surfaces: surfaceTypes,
bulks: bulkTypes,
detectors: detectorTypes,
internalSurfaces: internalSurfaceTypes,
regionRefractiveIndices
};
}
/**
* Derive the accepted wavelength range. By default this applies the scene's
* violet/red affine mapping to the standard 380/420/620/700 anchors. When
* non-visible light is retained, the closed range-estimation interval covers
* the positive finite f32 domain and runtime validation excludes zero.
*/
export function deriveWebGpuWavelengthRange({
violetWavelength = DEFAULT_VIOLET_WAVELENGTH,
redWavelength = DEFAULT_RED_WAVELENGTH,
keepNonVisibleLight = false
} = {}) {
if (
!Number.isFinite(violetWavelength) ||
!Number.isFinite(redWavelength) ||
!(redWavelength > violetWavelength)
) {
throw new RangeError(
'WebGPU violetWavelength and redWavelength must be finite, with redWavelength greater than violetWavelength.'
);
}
if (keepNonVisibleLight) {
// Zero is included conservatively for formula range estimation. Runtime
// source validation still requires the wavelength to be strictly positive.
return [[0, WEBGPU_F32_MAX]];
}
const scale = (
redWavelength - violetWavelength
) / (
DEFAULT_RED_WAVELENGTH - DEFAULT_VIOLET_WAVELENGTH
);
const mapWavelength = wavelength =>
violetWavelength +
(wavelength - DEFAULT_VIOLET_WAVELENGTH) * scale;
return [outwardF32Interval(
mapWavelength(BASE_UV_WAVELENGTH),
mapWavelength(BASE_INFRARED_WAVELENGTH),
'WebGPU wavelength range'
)];
}
/**
* Convert an ordinary instance parameter to the value that may be packed in
* an f32 buffer. Finite overflow and signed infinities saturate; NaN and
* non-numeric values remain scene-contract errors.
*/
export function clampWebGpuParameterToF32(value, name = 'parameter') {
if (typeof value !== 'number' || Number.isNaN(value)) {
throw new TypeError(`${name} must be a number other than NaN.`);
}
const clamped = Math.max(
-WEBGPU_F32_MAX,
Math.min(WEBGPU_F32_MAX, value)
);
return Math.fround(clamped);
}
/**
* Reject processed geometry which cannot use the WebGPU world-coordinate
* domain. This is intentionally a GPU preparation check, not common CPU
* preprocessing policy.
*/
export function validateWebGpuSpatialBounds(description) {
for (const [curveId, curve] of description.curves.entries()) {
validateSpatialBounds(curve.bounds, `curve ${curveId}`);
}
for (const [nodeId, node] of description.bvh.nodes.entries()) {
validateSpatialBounds(node.bounds, `BVH node ${nodeId}`);
}
}
/**
* Compare only range-dependent guard signatures. Structural DAG changes are
* intentionally outside this comparison and remain simulator-owned.
*
* @param {Object} estimated
* @param {Map<string, string>|null} previousSignatures
* @returns {Map<string, string>} Signatures to retain for the next update.
*/
export function recordWebGpuRecompilationNeeds(
estimated,
previousSignatures = null
) {
const nextSignatures = new Map();
for (const type of estimated.types) {
const previous = previousSignatures?.get(type.key);
type.recompilationNeeded = previousSignatures === null ||
previous === undefined ||
previous !== type.guardSignature;
type.recompilationReason = previousSignatures === null
? 'initial specialization'
: previous === undefined
? 'new type'
: previous !== type.guardSignature
? 'range guards changed'
: null;
nextSignatures.set(type.key, type.guardSignature);
}
estimated.removedTypeKeys = previousSignatures
? [...previousSignatures.keys()].filter(key => !nextSignatures.has(key))
: [];
estimated.anyRecompilationNeeded =
estimated.types.some(type => type.recompilationNeeded) ||
estimated.removedTypeKeys.length > 0;
return nextSignatures;
}
function createTypeRangeEntry({
type,
typeId,
instances,
reservedNames,
reservedRanges,
intervalLimit,
categoryName
}) {
const definition = type.definition;
const referencedNames = collectParameterNames(definition.dag);
const declaredNames = definition.paramNames ?? [];
validateUniqueNames(declaredNames, `${categoryName} type ${typeId} paramNames`);
const reservedSet = new Set(reservedNames);
for (const name of declaredNames) {
if (reservedSet.has(name)) {
throw new TypeError(
`${categoryName} type ${typeId} parameter ${JSON.stringify(name)} is reserved.`
);
}
}
const orderedNames = [
...declaredNames.filter(name => referencedNames.has(name)),
...reservedNames.filter(name => referencedNames.has(name))
];
const knownNames = new Set([...declaredNames, ...reservedNames]);
for (const name of referencedNames) {
if (!knownNames.has(name)) {
throw new TypeError(
`${categoryName} type ${typeId} DAG references undeclared parameter ${JSON.stringify(name)}.`
);
}
}
const parameters = orderedNames.map(name => {
if (declaredNames.includes(name)) {
return {
name,
range: collectInstanceParameterRange(
instances,
name,
intervalLimit,
`${categoryName} type ${typeId}`
)
};
}
const reserved = reservedRanges[name];
const rangeInfo = Array.isArray(reserved)
? { intervals: reserved, maybeInvalid: false }
: reserved;
if (!rangeInfo) {
throw new TypeError(
`Missing reserved WebGPU parameter range for ${JSON.stringify(name)}.`
);
}
return {
name,
range: cloneRange(rangeInfo.intervals),
...(rangeInfo.maybeInvalid
? { maybeInvalid: true, requiresFiniteGuard: true }
: {})
};
});
return {
kind: categoryName,
typeId,
key: `${categoryName}:${typeId}`,
name: definition.name,
parameters
};
}
function createInternalBoundaryType(
partialReflect,
refractiveIndexRange
) {
const ranges = {
d_0x: [[-1, 1]],
d_0y: [[-1, -WEBGPU_F32_MIN_NORMAL]],
P_0s: [[0, WEBGPU_MAX_POLARIZED_POWER]],
P_0p: [[0, WEBGPU_MAX_POLARIZED_POWER]],
n_0: cloneRangeInfo(refractiveIndexRange),
n_1: cloneRangeInfo(refractiveIndexRange)
};
const parameters = INTERNAL_BOUNDARY_PARAMETERS.map(name => {
const rangeInfo = Array.isArray(ranges[name])
? { intervals: ranges[name], maybeInvalid: false }
: ranges[name];
return {
name,
range: cloneRange(rangeInfo.intervals),
...(rangeInfo.maybeInvalid
? { maybeInvalid: true, requiresFiniteGuard: true }
: {})
};
});
const guardProfile = {
finiteRefractiveIndex:
parameters.some(parameter =>
(parameter.name === 'n_0' || parameter.name === 'n_1') &&
parameter.maybeInvalid
)
};
return {
kind: 'surface',
internal: true,
key: partialReflect
? 'regionBoundaryPartialReflection'
: 'regionBoundaryRefraction',
name: partialReflect
? 'Region boundary with partial reflection'
: 'Region boundary without partial reflection',
partialReflect,
outRayCount: partialReflect ? 2 : 1,
parameters,
specialization: {
kind: 'internalSurface',
guardProfile,
guardSignature: JSON.stringify(guardProfile)
},
guardSignature: JSON.stringify(guardProfile)
};
}
function attachDagSpecialization(type, dag) {
type.specialization = createDagWgslSpecialization(dag, {
parameters: type.parameters
});
type.guardSignature = type.specialization.guardSignature;
}
function validateSourceCoordinateOutputs(type, dag) {
const labels = collectNodeLabels(dag);
for (const label of ['x', 'y']) {
const nodeId = labels.get(label);
if (nodeId === undefined) {
throw new TypeError(`source type ${type.typeId} has no ${label} output.`);
}
const range = type.specialization.rangeResult.nodeRanges[nodeId];
for (const [minimum, maximum] of range.intervals) {
if (minimum < -WEBGPU_SPATIAL_MAX || maximum > WEBGPU_SPATIAL_MAX) {
throw new RangeError(
`source type ${type.typeId} ${label} output may exceed the ` +
`WebGPU spatial limit ${WEBGPU_SPATIAL_MAX}.`
);
}
}
}
}
function collectInstanceParameterRange(
instances,
name,
intervalLimit,
context
) {
const values = instances.map((instance, instanceIndex) => {
if (!Object.prototype.hasOwnProperty.call(instance.params, name)) {
throw new TypeError(
`${context} instance ${instanceIndex} is missing parameter ${JSON.stringify(name)}.`
);
}
return clampWebGpuParameterToF32(
instance.params[name],
`${context} instance ${instanceIndex} parameter ${JSON.stringify(name)}`
);
});
if (values.length === 0) {
throw new TypeError(`${context} has no instances from which to estimate parameters.`);
}
return simplifySingletonValues(values, intervalLimit);
}
function createSourceReservedRanges(sources, intervalLimit) {
const counts = sources.map((source, sourceIndex) => {
if (
!Number.isSafeInteger(source.rayCount) ||
source.rayCount < 0
) {
throw new RangeError(
`source instance ${sourceIndex} rayCount must be a nonnegative safe integer.`
);
}
return source.rayCount;
});
const nRange = simplifySingletonValues(
counts.map(count => clampWebGpuParameterToF32(count, 'source rayCount')),
intervalLimit
);
const maximumIndex = counts.reduce(
(maximum, count) => Math.max(maximum, count - 1),
0
);
return {
i: [[0, Math.fround(maximumIndex)]],
N: nRange
};
}
function estimateRegionRefractiveIndices(
description,
regionBounds,
wavelengthRange,
intervalLimit
) {
return description.regions.map((region, regionId) => {
const definition = description.types.bulks[
region.bulkTypeId
].definition;
const bounds = regionBounds[regionId];
const coordinateRanges = bounds
? {
x: [outwardF32Interval(
bounds.minX,
bounds.maxX,
`region ${regionId} x range`
)],
y: [outwardF32Interval(
bounds.minY,
bounds.maxY,
`region ${regionId} y range`
)]
}
: zeroCoordinateRanges();
const parameterRanges = createRegionDagParameterRanges(
definition,
region,
coordinateRanges,
wavelengthRange,
regionId
);
const nNodeId = collectNodeLabels(definition.dag).get('n');
if (nNodeId === undefined) {
throw new TypeError(`bulk type ${region.bulkTypeId} has no n output.`);
}
const estimated = estimateDagRanges(
definition.dag,
parameterRanges
).nodeRanges[nNodeId];
return {
regionId,
bulkTypeId: region.bulkTypeId,
intervals: simplifyIntervals(estimated.intervals, intervalLimit),
maybeInvalid: estimated.maybeInvalid
};
});
}
function createRegionDagParameterRanges(
definition,
region,
coordinateRanges,
wavelengthRange,
regionId
) {
const declaredNames = definition.paramNames ?? [];
const declaredSet = new Set(declaredNames);
const reservedRanges = {
...coordinateRanges,
lambda: wavelengthRange
};
const ranges = {};
for (const name of collectParameterNames(definition.dag)) {
if (declaredSet.has(name)) {
if (!Object.prototype.hasOwnProperty.call(region.params, name)) {
throw new TypeError(
`region ${regionId} is missing parameter ${JSON.stringify(name)}.`
);
}
const value = clampWebGpuParameterToF32(
region.params[name],
`region ${regionId} parameter ${JSON.stringify(name)}`
);
ranges[name] = [[value, value]];
} else if (reservedRanges[name]) {
ranges[name] = cloneRange(reservedRanges[name]);
} else {
throw new TypeError(
`bulk type ${region.bulkTypeId} DAG references undeclared parameter ${JSON.stringify(name)}.`
);
}
}
return ranges;
}
function interactionRefractiveIndexRange(
curves,
regionRefractiveIndices,
regionBounds,
intervalLimit
) {
const perCurveRanges = curves.map(curve => {
const possibleRegions = regionRefractiveIndices.filter(info => {
const bounds = regionBounds[info.regionId];
return bounds && boundsOverlap(curve.bounds, bounds);
});
let effective = {
intervals: [[1, 1]],
maybeInvalid: false
};
for (const regionInfo of possibleRegions) {
const optionalRegion = {
intervals: simplifyIntervals(
[[1, 1], ...regionInfo.intervals],
intervalLimit
),
maybeInvalid: regionInfo.maybeInvalid
};
effective = multiplyRangeInfos(
effective,
optionalRegion,
intervalLimit
);
}
return effective;
});
return unionRangeInfos(perCurveRanges, intervalLimit);
}
function multiplyRangeInfos(left, right, intervalLimit) {
if (left.intervals.length === 0 || right.intervals.length === 0) {
return {
intervals: [],
maybeInvalid: true
};
}
const estimated = estimateDagRanges(MULTIPLY_RANGE_DAG, {
left: left.intervals,
right: right.intervals
}).nodeRanges[MULTIPLY_RANGE_DAG.root];
return {
intervals: simplifyIntervals(estimated.intervals, intervalLimit),
maybeInvalid:
left.maybeInvalid || right.maybeInvalid || estimated.maybeInvalid
};
}
function unionRangeInfos(infos, intervalLimit) {
if (infos.length === 0) {
return {
intervals: [[1, 1]],
maybeInvalid: false
};
}
return {
intervals: simplifyIntervals(
infos.flatMap(info => info.intervals),
intervalLimit
),
maybeInvalid: infos.some(info => info.maybeInvalid)
};
}
function createInteractionReservedRanges({
coordinateRanges,
curves,
wavelengthRange,
refractiveIndexRange,
tolerancePolicy,
intervalLimit
}) {
return {
d_0x: [[-1, 1]],
d_0y: [[-1, -WEBGPU_F32_MIN_NORMAL]],
P_0s: [[0, WEBGPU_MAX_POLARIZED_POWER]],
P_0p: [[0, WEBGPU_MAX_POLARIZED_POWER]],
lambda: wavelengthRange,
...(coordinateRanges ?? zeroCoordinateRanges()),
u: collectCurveParameterRanges(curves, tolerancePolicy, intervalLimit),
sigma: curves.some(curve => curve.twoSided)
? [[-1, -1], [1, 1]]
: [[1, 1]],
...(refractiveIndexRange
? {
n_0: cloneRangeInfo(refractiveIndexRange),
n_1: cloneRangeInfo(refractiveIndexRange)
}
: {})
};
}
function collectCurveParameterRanges(curves, tolerancePolicy, intervalLimit) {
if (curves.length === 0) return [[0, 0]];
const intervals = curves.map(curve => {
const geometry = curve.geometry;
if (geometry.kind === 'circle') return [0.5, 0.5];
const distanceTolerance = Math.max(
geometry.positionTolerance,
geometry.endpointTolerance ?? 0
);
if (
geometry.kind === 'lineSegment' ||
geometry.kind === 'smoothLineSegment'
) {
const tolerance = Math.max(
tolerancePolicy.parameter,
distanceTolerance * geometry.invLength
);
return outwardF32Interval(-tolerance, 1 + tolerance, 'curve u range');
}
if (geometry.kind === 'circularArc') {
const tolerance = Math.max(
tolerancePolicy.parameter,
distanceTolerance * geometry.invChordLength
);
return outwardF32Interval(-tolerance, 1 + tolerance, 'curve u range');
}
if (geometry.kind === 'cubicBezier') {
const normalizedTolerance = distanceTolerance * geometry.invScale;
const startDerivative = Math.hypot(
3 * (geometry.control1X - geometry.startX),
3 * (geometry.control1Y - geometry.startY)
);
const endDerivative = Math.hypot(
3 * (geometry.endX - geometry.control2X),
3 * (geometry.endY - geometry.control2Y)
);
const startTolerance = startDerivative > 0
? Math.max(tolerancePolicy.parameter, normalizedTolerance / startDerivative)
: tolerancePolicy.parameter;
const endTolerance = endDerivative > 0
? Math.max(tolerancePolicy.parameter, normalizedTolerance / endDerivative)
: tolerancePolicy.parameter;
return outwardF32Interval(
-startTolerance,
1 + endTolerance,
'curve u range'
);
}
throw new TypeError(
`Unsupported prepared curve kind: ${JSON.stringify(geometry.kind)}`
);
});
return simplifyIntervals(intervals, intervalLimit);
}
function collectOwnerCoordinateRanges(description, intervalLimit) {
const surfaceBounds = description.surfaces.map(() => null);
const regionBounds = description.regions.map(() => null);
const detectorBounds = description.detectors.map(() => null);
for (const curve of description.curves) {
const table = curve.ownerKind === 'surface'
? surfaceBounds
: curve.ownerKind === 'region'
? regionBounds
: detectorBounds;
table[curve.ownerId] = combineBounds(table[curve.ownerId], curve.bounds);
}
return {
surfaces: groupBoundsByType(
surfaceBounds,
description.surfaces,
'surfaceTypeId',
description.types.surfaces.length,
intervalLimit
),
bulks: groupBoundsByType(
regionBounds,
description.regions,
'bulkTypeId',
description.types.bulks.length,
intervalLimit
),
detectors: groupBoundsByType(
detectorBounds,
description.detectors,
'detectorTypeId',
description.types.detectors.length,
intervalLimit
),
surfaceBounds,
regionBounds,
detectorBounds
};
}
function groupBoundsByType(
ownerBounds,
owners,
typeIdKey,
typeCount,
intervalLimit
) {
const grouped = Array.from({ length: typeCount }, () => []);
ownerBounds.forEach((bounds, ownerId) => {
if (bounds) grouped[owners[ownerId][typeIdKey]].push(bounds);
});
return grouped.map(boundsList => {
if (boundsList.length === 0) return zeroCoordinateRanges();
return {
x: simplifyIntervals(boundsList.map(bounds =>
outwardF32Interval(bounds.minX, bounds.maxX, 'coordinate x range')
), intervalLimit),
y: simplifyIntervals(boundsList.map(bounds =>
outwardF32Interval(bounds.minY, bounds.maxY, 'coordinate y range')
), intervalLimit)
};
});
}
function simplifySingletonValues(values, intervalLimit) {
const unique = [...new Set(values.map(value =>
Object.is(value, -0) ? 0 : value
))].sort((left, right) => left - right);
if (unique.length === 0) return [[0, 0]];
if (unique.length > intervalLimit) {
return [[unique[0], unique[unique.length - 1]]];
}
return unique.map(value => [value, value]);
}
function simplifyIntervals(intervals, intervalLimit) {
const sorted = intervals
.map(([lo, hi]) => [lo, hi])
.sort((left, right) => left[0] - right[0] || left[1] - right[1]);
const merged = [];
for (const interval of sorted) {
const last = merged[merged.length - 1];
if (last && interval[0] <= last[1]) {
last[1] = Math.max(last[1], interval[1]);
} else {
merged.push(interval);
}
}
if (merged.length > intervalLimit) {
return [[merged[0][0], merged[merged.length - 1][1]]];
}
return merged;
}
function outwardF32Interval(lo, hi, name) {
if (!Number.isFinite(lo) || !Number.isFinite(hi) || lo > hi) {
throw new RangeError(`${name} must have finite ordered endpoints.`);
}
if (lo < -WEBGPU_F32_MAX || hi > WEBGPU_F32_MAX) {
throw new RangeError(`${name} is outside the finite f32 range.`);
}
let roundedLo = Math.fround(lo);
let roundedHi = Math.fround(hi);
if (roundedLo > lo) roundedLo = nextDownF32(roundedLo);
if (roundedHi < hi) roundedHi = nextUpF32(roundedHi);
return [roundedLo, roundedHi];
}
function nextUpF32(value) {
value = Math.fround(value);
if (value === Infinity) return value;
if (Object.is(value, -0)) value = 0;
F32[0] = value;
if (value >= 0) U32[0] += 1;
else U32[0] -= 1;
return F32[0];
}
function nextDownF32(value) {
return -nextUpF32(-value);
}
function validateSpatialBounds(bounds, name) {
if (!bounds) {
throw new TypeError(`Processed ${name} is missing bounds.`);
}
for (const key of ['minX', 'minY', 'maxX', 'maxY']) {
const value = bounds[key];
if (!Number.isFinite(value) || Math.abs(value) > WEBGPU_SPATIAL_MAX) {
throw new RangeError(
`Processed ${name} ${key}=${value} exceeds the WebGPU spatial limit ${WEBGPU_SPATIAL_MAX}.`
);
}
}
if (bounds.minX > bounds.maxX || bounds.minY > bounds.maxY) {
throw new RangeError(`Processed ${name} has invalid bounds.`);
}
}
function combineBounds(first, second) {
if (!first) return { ...second };
return {
minX: Math.min(first.minX, second.minX),
minY: Math.min(first.minY, second.minY),
maxX: Math.max(first.maxX, second.maxX),
maxY: Math.max(first.maxY, second.maxY)
};
}
function boundsOverlap(first, second) {
return (
first.minX <= second.maxX &&
first.maxX >= second.minX &&
first.minY <= second.maxY &&
first.maxY >= second.minY
);
}
function zeroCoordinateRanges() {
return { x: [[0, 0]], y: [[0, 0]] };
}
function cloneRange(range) {
if (!range) throw new TypeError('Missing reserved WebGPU parameter range.');
return range.map(([lo, hi]) => [lo, hi]);
}
function cloneRangeInfo(info) {
return {
intervals: cloneRange(info.intervals),
maybeInvalid: info.maybeInvalid
};
}
function validateUniqueNames(names, context) {
const unique = new Set();
for (const name of names) {
if (typeof name !== 'string' || name.length === 0 || unique.has(name)) {
throw new TypeError(`${context} must contain unique nonempty strings.`);
}
unique.add(name);
}
}
function validateDescription(description) {
if (
!description ||
!description.types ||
!Array.isArray(description.curves) ||
!description.bvh
) {
throw new TypeError('A processed scene description is required.');
}
}
function validateIntervalLimit(intervalLimit) {
if (!Number.isInteger(intervalLimit) || intervalLimit < 1) {
throw new RangeError('intervalLimit must be a positive integer.');
}
}