@photivra/engine 2.7.0 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +86 -1
- package/CITATION.cff +1 -1
- package/README.md +6 -6
- package/SECURITY.md +1 -1
- package/dist/capture/native-environment-lean.d.ts +35 -5
- package/dist/capture/native-environment-lean.js +175 -16
- package/dist/capture/native-environment-lean.js.map +1 -1
- package/dist/capture/native-environment-raw.d.ts +9 -1
- package/dist/capture/native-environment-raw.js +14 -9
- package/dist/capture/native-environment-raw.js.map +1 -1
- package/dist/capture/native-environment-tabulated.d.ts +58 -0
- package/dist/capture/native-environment-tabulated.js +91 -0
- package/dist/capture/native-environment-tabulated.js.map +1 -0
- package/dist/core/internal-radiance-evaluator.d.ts +28 -0
- package/dist/core/internal-radiance-evaluator.js +30 -0
- package/dist/core/internal-radiance-evaluator.js.map +1 -1
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/core/version.js.map +1 -1
- package/dist/facade/diffraction-provider.js +30 -2
- package/dist/facade/diffraction-provider.js.map +1 -1
- package/dist/facade/display-view.d.ts +14 -83
- package/dist/facade/display-view.js +331 -846
- package/dist/facade/display-view.js.map +1 -1
- package/dist/facade/expected-signal-setup.d.ts +65 -0
- package/dist/facade/expected-signal-setup.js +56 -0
- package/dist/facade/expected-signal-setup.js.map +1 -0
- package/dist/facade/expected-signal.d.ts +83 -0
- package/dist/facade/expected-signal.js +571 -0
- package/dist/facade/expected-signal.js.map +1 -0
- package/dist/facade/scene-radiance.d.ts +6 -0
- package/dist/facade/scene-radiance.js +31 -10
- package/dist/facade/scene-radiance.js.map +1 -1
- package/dist/facade/simulate-capture.js +65 -16
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.js +26 -49
- package/dist/facade/simulate-display-view.js.map +1 -1
- package/docs/API_REFERENCE.md +1 -1
- package/docs/QUICK_START.md +1 -1
- package/docs/api/core-version.md +1 -1
- package/docs/api/exports.json +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,91 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
### Faster, same output
|
|
6
|
+
|
|
7
|
+
- **Display view: a sample's terms written in place; the clock read less often.** Each real pixel's recorded terms went through a new typed-array view (`subarray`, then `fill`) and `Date.now()` was read per display pixel. The terms are now written straight into the real pixel's memo slot (a scratch buffer only for the first sample of a one-material key), and the clock is read every 16 display pixels and at each row's end; yield timing never changes the output.
|
|
8
|
+
- **Same values:** 57 display views (Speed, a 2 × 2 box aperture and the Quality options; three presets, a caller material scene and a caller function scene; exact and stratified sizes) hash identically before and after: samples, expected values, noise and diagnostics. All hash locks pass.
|
|
9
|
+
- **Measured:** four 32-row bands of the app's 2028 × 1352 view of a 24 MP frame (a caller material scene, Speed, 2 × 2 box aperture), one thread, median of five alternating runs: 4.31 s → 4.00 s (about 7 %). A line profile puts the rest of the time in the per-real-pixel statistics and the scene's material lookups (the caller's `materialAt` with the frozen point it receives), which these changes leave as they are.
|
|
10
|
+
|
|
11
|
+
## 2.8.0 — unreleased (prepared October 25, 2026)
|
|
12
|
+
|
|
13
|
+
A minor release that makes full-resolution captures of chart and material scenes about 50× faster (48–84× in the regions measured), and makes the display view exact at device-pixel sizes.
|
|
14
|
+
- **Faster captures, same bytes:** for chart presets and caller material scenes with a still, rectilinear camera, the RAW stage takes tabulated expected electrons and skips the per-site record pipeline. On every request tested, the samples, RAW codes and files are the bytes of 2.7.0.
|
|
15
|
+
- **Exact display view at device-pixel sizes:** a display pixel covering at most 25 real pixels is the exact area mean of the real pixels instead of a stratified estimate. Those views have new values (a deliberate reference change); the 155-case validation is the same as 2.6.0's or better.
|
|
16
|
+
- **Visible changes:** chart and material captures report a `raw.expectedSignal` assumption and count far fewer `providerEvaluationCount` evaluations; auto white balance no longer renders the RAW twice. Root exports are unchanged (1,727).
|
|
17
|
+
- **Known limit:** the Quality noise limit of 2.6.0 remains (#379), and its fix moves to 2.9.0.
|
|
18
|
+
|
|
19
|
+
Measured on one thread (24 MP frame, 6 µm, 50 mm f/8, 2 × 2 pixel averaging), with identical samples: a 512 × 512 Siemens star region takes 0.6 s instead of 31.7 s on 2.7.0 (Quality options: 2.8 s instead of 135 s); the whole frame takes 28.2 s as sRGB and 36.5 s as a JPEG.
|
|
20
|
+
|
|
21
|
+
### Faster, same output
|
|
22
|
+
|
|
23
|
+
- **Captures of chart and material scenes: tabulated expected electrons, 10–13× faster again; the same bytes on every frame tested (#394, step 2).**
|
|
24
|
+
- **What changes:** a capture of a chart preset or a caller material scene, with a still, rectilinear, distortion-free camera (no rotation, lens projection or distortion, vignetting, pupil sampling or caller PSF), no longer runs each native site through the environment chain. It takes each site's expected photo electrons from the display view's tabulated expected signal: the same sum over the pixel-aperture nodes, anti-aliasing spots, diffraction taps and spectral nodes, formed in another order. The sensor stage is the native one, with each site's own seeds: dark charge, both Poisson draws, read noise and the ADC.
|
|
25
|
+
- **Same result, by verification:** one site of each CFA phase still runs the full chain. Its readout must be reproduced bit for bit, and its tabulated electrons must agree with the chain's within 1e-9 relative; otherwise the capture takes the environment path. Electrons that differ in the last bit could, rarely, draw a different integer at a site; on every frame tested none did.
|
|
26
|
+
- **Measured equality:** 0 differing RAW codes and 0 differing samples:
|
|
27
|
+
- 1,052,676 native sites of a 24 MP frame in Speed, for dead leaves and for a material scene;
|
|
28
|
+
- 272,484 sites with the Quality options;
|
|
29
|
+
- every frame of the 2.0.0 hash locks.
|
|
30
|
+
|
|
31
|
+
The verification sites agreed within 2 × 10⁻¹⁵ relative.
|
|
32
|
+
- **Visible changes:**
|
|
33
|
+
- a `raw.expectedSignal` assumption names the method and the verification;
|
|
34
|
+
- `diagnostics.providerEvaluationCount` counts the verification sites' evaluator calls and the table's scene lookups (120 instead of 720 for a 6 × 6 frame);
|
|
35
|
+
- auto white balance keeps the RAW instead of rendering it again, because its codes do not depend on the white-balance intent.
|
|
36
|
+
|
|
37
|
+
The 2.0.0 hash lock's byte digests are unchanged; its assumption and diagnostics digests were re-recorded.
|
|
38
|
+
- **Measured** on a 512 × 512 centre region of a 24 MP frame (6 µm, 50 mm f/8, 2 × 2 pixel averaging), one thread, one run each, against main with step 1:
|
|
39
|
+
|
|
40
|
+
| Scene | Speed | Quality options |
|
|
41
|
+
| --- | --- | --- |
|
|
42
|
+
| Siemens star preset | 6.5 → 0.6 s | 37.5 → 2.8 s |
|
|
43
|
+
| Siemens star as a caller material scene | 7.1 → 0.7 s | 40.4 → 3.0 s |
|
|
44
|
+
|
|
45
|
+
The whole 24 MP frame on one thread now takes 28.2 s as sRGB (RAW 24.2 s, development 3.9 s), 36.5 s as a JPEG (encoding 11.2 s) and 75.7 s as a Quality PNG.
|
|
46
|
+
- **Code:**
|
|
47
|
+
- the expected signal moved to `src/facade/expected-signal.ts`, so capture-only bundles still leave out the display view;
|
|
48
|
+
- the input setup is shared with the display view in `src/facade/expected-signal-setup.ts`, and the display view's hash locks are unchanged;
|
|
49
|
+
- the RAW task is in `src/capture/native-environment-tabulated.ts`.
|
|
50
|
+
|
|
51
|
+
- **Captures of chart and material scenes: 3.6–8.3× faster RAW stage, no byte changed (#394, step 1).**
|
|
52
|
+
- **Engine-built tiles without a private copy:** the native environment no longer `structuredClone`s the site records that `simulateCapture` builds for each tile. They are fresh engine records, never shown to supplied code or changed afterwards, so they are planned as engine-owned data directly. Shared records keep their identity, which also makes the template comparisons cheaper. Tiles from callers are still copied.
|
|
53
|
+
- **Key taps:** for a chart preset or a caller material scene, the engine-built evaluator's result for a query is a pure function of the material at the queried plane point and the wavelength (with object-space diffraction, of the convolved lattice site and the wavelength). A verified lean template now plans its sites' taps as those keys instead of frozen requests, and takes each radiance from the key: chart materials from a table filled once per material and spectral node, lattice sites from the convolved band. Without a PSF a tap's key is the same at every spectral node, so it is found once per spatial node. The evaluator is not called for such taps; the evaluation count is still added, and the abort check still runs.
|
|
54
|
+
- **Verified per template:** a template takes key taps only when its own site's key radiances equal every radiance of its full execution, tap by tap (`Object.is`). Key taps never apply with pupil rays, a lens field mapping, a caller reflectance or radiance function, or a batched provider. A tap that would need the full irradiance function (a non-finite or negative value, or a field throughput outside the fast path) sends its site through the full path before anything is counted. A caller `materialAt` that throws during key planning sends its site to the full path, which throws the same error.
|
|
55
|
+
- **Tests:** a new test compares lean and full captures (samples, diagnostics counts, assumptions) for chart presets and caller material scenes in Speed and Quality, with a fixed-size chart, rotation, two temporal samples, a rolling shutter, vignetting and a region, and checks that key taps ran; and that lens field mappings and caller function scenes stay on request taps. All hash locks and facade fingerprints are unchanged.
|
|
56
|
+
- **Measured** on a 512 × 512 centre region of a 24 MP frame (6 µm, 50 mm f/8, 2 × 2 pixel averaging), one thread, main before and this change after, one run each; samples identical:
|
|
57
|
+
|
|
58
|
+
| Scene | Speed | Quality options (diffraction, four-spot filter, AHD) |
|
|
59
|
+
| --- | --- | --- |
|
|
60
|
+
| Siemens star preset | 31.7 → 6.5 s (4.9×) | 135.2 → 37.5 s (3.6×) |
|
|
61
|
+
| Siemens star as a caller material scene | 58.6 → 7.1 s (8.3×) | 252.9 → 40.4 s (6.3×) |
|
|
62
|
+
|
|
63
|
+
The material scene gains more because it now also takes the engine-built evaluator's shorter result parsing that chart presets had since 2.6.
|
|
64
|
+
|
|
65
|
+
### Changed
|
|
66
|
+
|
|
67
|
+
- **Display view: the exact area mean where a footprint holds at most 25 real pixels (#393).** This is a deliberate reference change.
|
|
68
|
+
- **Before:** each display pixel's expected value was a stratified estimate over K × K jittered real pixels.
|
|
69
|
+
- **Now:** where a display pixel's rectangle holds at most 25 real pixels (and at most `view.samplesPerAxis`²), so views at least 1/5 of the frame's width, every real pixel it touches is evaluated once and weighted by its exact overlap, w_j / A. The expected value is then the definition itself, D_P = Σ_j (w_j/A)·E[L_j], with no sampling error.
|
|
70
|
+
- **Noise:** the linear part keeps the closed-form B_c. The independent residual becomes Σ_j (w_j/A)²·r_j, the same model written exactly.
|
|
71
|
+
- **Shared terms:** a real pixel's recorded terms are shared by the display pixels that touch it, in a ring of native rows.
|
|
72
|
+
- **Unchanged:** coarser views keep the stratified estimator and their bytes. The `view.estimation` assumption names the method, and the internal render result reports it.
|
|
73
|
+
- **Tests:** a new test checks, in Speed and with diffraction, the four-spot filter and AHD, that a half-size and a 2/3-size view of a 48 × 32 frame have exactly the overlap-weighted means of the frame-size view's per-pixel expected values (within 1e-12 relative). Two of the six display-view hash locks and both memo locks were re-recorded (their views take the exact path); the four coarser locks are unchanged.
|
|
74
|
+
- **Measured** on the 2028 × 1352 view's 8-row strips of a 24 MP frame, the app's chart as a material scene, one thread, against 2.7.0:
|
|
75
|
+
|
|
76
|
+
| Strip | Speed | Quality options |
|
|
77
|
+
| --- | --- | --- |
|
|
78
|
+
| Rows 660 / 300 / 1000 | 1.04 / 1.01 / 0.97 s → 0.84 / 0.85 / 0.77 s | 2.9 / 3.1 / 2.7 s → 2.3 / 2.8 / 2.3 s |
|
|
79
|
+
|
|
80
|
+
Samples fell 3–9× and site evaluations 3–10×. The time fell less, because the 2.7 memos had already made repeated samples cheap: at device-pixel sizes the view now does about one real pixel's work per real pixel.
|
|
81
|
+
- **Why 25:** measured on 8-row strips of a 24 MP view (Siemens star and dead leaves, f/2.8, one thread, one run each), the exact path took the stratified estimator's time within run-to-run spread up to 25 real pixels per display pixel and longer above: 1.1–1.2× at 36, 1.4–1.5× at 64, 1.5–2.4× at 144, because it evaluates every real pixel while the estimator takes a quarter of its samples per axis where the pilot finds no detail.
|
|
82
|
+
- **Validation:** the 155-case validation was re-run on October 25 (`de77074`, [record](https://github.com/Photivra/photivra/blob/main/docs/validation/display-view-2026-10-25/README.md)): 145 cases are identical to the 2.6.0 record, and the 10 cases on the exact area mean (the phone at 800 wide) are the same or better (Siemens star: expected-value RMS 1.78 → 1.35 codes, SSIM 0.9931 → 0.9966). The known Quality noise miss of case 138 (#379) remains: 1.165 against the 1.15 limit.
|
|
83
|
+
|
|
84
|
+
### Known limit
|
|
85
|
+
|
|
86
|
+
- **Quality noise at small footprints (#379), unchanged from 2.6.0.** With `output.demosaic: "ahd"` the display view's noise has too much colour where a display pixel covers few sensor pixels; validation case 138 is outside its limit (1.165 against 1.15). The exact covariance through AHD's two linear candidates was measured and makes it worse (1.351), because AHD's direction choice reacts to the noise; the remaining fix is a Monte Carlo model of the real AHD output's noise covariance, moved to 2.9.0 by the owner's decision of October 25, 2026. Speed (bilinear) is exact, and the Photivra site offers Speed only.
|
|
87
|
+
|
|
88
|
+
## 2.7.0 — published October 24, 2026
|
|
4
89
|
|
|
5
90
|
A minor release that makes the display view much faster at device-pixel sizes and adds two APIs for it.
|
|
6
91
|
- **API for regions:** `prepareDisplayView` resolves a request once for many regions.
|
package/CITATION.cff
CHANGED
|
@@ -9,7 +9,7 @@ abstract: "An open TypeScript engine for the science and mathematics of photogra
|
|
|
9
9
|
repository-code: "https://github.com/photivra/photivra"
|
|
10
10
|
url: "https://photivra.com"
|
|
11
11
|
license: "Apache-2.0"
|
|
12
|
-
version: "2.
|
|
12
|
+
version: "2.8.0"
|
|
13
13
|
keywords:
|
|
14
14
|
- photography
|
|
15
15
|
- camera optics
|
package/README.md
CHANGED
|
@@ -32,10 +32,10 @@ The source is licensed under Apache-2.0.
|
|
|
32
32
|
|
|
33
33
|
## Quick start
|
|
34
34
|
|
|
35
|
-
Install the published ESM package below. The [2.
|
|
35
|
+
Install the published ESM package below. The [2.8.0 release record](https://github.com/Photivra/photivra/blob/main/docs/RELEASE_2_8_0.md) and [changelog](https://github.com/Photivra/photivra/blob/main/CHANGELOG.md) describe the current release:
|
|
36
36
|
|
|
37
37
|
```sh
|
|
38
|
-
npm install @photivra/engine@2.
|
|
38
|
+
npm install @photivra/engine@2.8.0
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
Simulate a photograph of a test chart with a generic camera, then read the developed sRGB pixels:
|
|
@@ -75,7 +75,7 @@ console.log(horizontal.provenance);
|
|
|
75
75
|
|
|
76
76
|
Start with the [Developer Guide](https://github.com/Photivra/photivra/blob/main/docs/DEVELOPERS.md). Then use the [Usage Guide](https://github.com/Photivra/photivra/blob/main/docs/USAGE.md) and the [API reference](https://github.com/Photivra/photivra/blob/main/docs/API_REFERENCE.md), which is generated from the package exports.
|
|
77
77
|
|
|
78
|
-
Repository `main` may describe newer, unreleased work. For the stable contract, use the docs bundled in the published package, or the exact `v2.
|
|
78
|
+
Repository `main` may describe newer, unreleased work. For the stable contract, use the docs bundled in the published package, or the exact `v2.8.0` tag.
|
|
79
79
|
|
|
80
80
|
## Documentation
|
|
81
81
|
|
|
@@ -363,13 +363,13 @@ The application can then use those results, while the photography calculations t
|
|
|
363
363
|
|
|
364
364
|
## Status
|
|
365
365
|
|
|
366
|
-
- Repository package version: `2.
|
|
367
|
-
- Engine API contract: `2.
|
|
366
|
+
- Repository package version: `2.8.0` (release candidate prepared October 25, 2026; publication is separate)
|
|
367
|
+
- Engine API contract: `2.8.0`
|
|
368
368
|
- Versioned generic tier reference assets: [GENERIC_TIER_PRESETS.md](https://github.com/Photivra/photivra/blob/main/docs/GENERIC_TIER_PRESETS.md).
|
|
369
369
|
- Composed POC simulation API contract: `1.0.0`
|
|
370
370
|
- Stability: 1.0 root-package compatibility policy; bounded scientific models and independent POC contract
|
|
371
371
|
|
|
372
|
-
From 1.0.1, the package version and `ENGINE_API_VERSION` are equal. New captures/plans record 2.
|
|
372
|
+
From 1.0.1, the package version and `ENGINE_API_VERSION` are equal. New captures/plans record 2.8.0; existing archives keep their original creator identities. Schema, model and POC contract versions stay independent.
|
|
373
373
|
|
|
374
374
|
A breaking change to the root contracts now needs a new package major version. Additive and corrective changes follow [compatibility and migration](https://github.com/Photivra/photivra/blob/main/docs/RELEASE_1_3_0.md). A 1.0 package is not a claim of calibrated physical accuracy.
|
|
375
375
|
|
package/SECURITY.md
CHANGED
|
@@ -4,7 +4,7 @@ Please do not open a public issue that contains vulnerability details.
|
|
|
4
4
|
|
|
5
5
|
## Supported versions
|
|
6
6
|
|
|
7
|
-
The published 1.0.0 release started the 1.x root-package compatibility line, and 2.0.0 started the 2.x line. The current release is **2.
|
|
7
|
+
The published 1.0.0 release started the 1.x root-package compatibility line, and 2.0.0 started the 2.x line. The current release is **2.7.0**, published on October 24, 2026 (see [CHANGELOG.md](CHANGELOG.md)).
|
|
8
8
|
|
|
9
9
|
Security fixes target the current maintained development and release line. Do not assume that older 1.x releases, snapshots or pre-1.0 releases will receive fixes.
|
|
10
10
|
|
|
@@ -32,14 +32,15 @@
|
|
|
32
32
|
import type { SimulateEnvironmentSensorRawFrameInput } from "./environment-raw-producer.js";
|
|
33
33
|
import type { planEnvironmentSensorPhotoSignal, executeEnvironmentSensorPhotoSignal, EnvironmentRadianceEvaluator, EnvironmentApertureRadianceEvaluator } from "../sensor/environment-photo-signal.js";
|
|
34
34
|
import type { SensorEqeTemporalPhotoSignal } from "../sensor/temporal-photo-signal.js";
|
|
35
|
+
import { type InternalRadianceAccess } from "../core/internal-radiance-evaluator.js";
|
|
35
36
|
import { type SceneRadianceEvaluationRequest } from "../schema/scene-radiance.js";
|
|
36
37
|
import { type PreparedEnvironmentLensFieldMapping } from "../optics/sensor-environment-query.js";
|
|
37
38
|
import { type ResolveLensSampledPsfInput } from "../optics/lens-psf-profile.js";
|
|
38
39
|
import { type SensorApertureRay } from "../optics/sensor-aperture-rays.js";
|
|
39
40
|
import { parseFocusPlane } from "../optics/focus-state.js";
|
|
40
41
|
import { prepareCaptureExposureWindowSampler } from "../sensor/exposure-window.js";
|
|
41
|
-
import { type NativeRawProducerFrameContext } from "./sensor-raw-producer.js";
|
|
42
|
-
import type { NativeRawLeanSensorHook } from "./native-raw.js";
|
|
42
|
+
import { calculateNativeRawSite, type NativeRawProducerFrameContext, type SensorRawProducerSiteInput } from "./sensor-raw-producer.js";
|
|
43
|
+
import type { NativeRawLeanOutput, NativeRawLeanSensorHook } from "./native-raw.js";
|
|
43
44
|
type Site = SimulateEnvironmentSensorRawFrameInput["sites"][number];
|
|
44
45
|
type Env = Site["environment"];
|
|
45
46
|
type FullPlan = ReturnType<typeof planEnvironmentSensorPhotoSignal>;
|
|
@@ -51,6 +52,7 @@ export declare const nativeEnvironmentLeanStatistics: {
|
|
|
51
52
|
executedSites: number;
|
|
52
53
|
sensorSites: number;
|
|
53
54
|
psfFallbacks: number;
|
|
55
|
+
keySites: number;
|
|
54
56
|
};
|
|
55
57
|
export declare function setNativeEnvironmentLeanExecutionEnabled(enabled: boolean): void;
|
|
56
58
|
interface SpatialNode {
|
|
@@ -86,6 +88,13 @@ interface Template {
|
|
|
86
88
|
execution?: ExecutionConstants;
|
|
87
89
|
/** undefined until the template site executed through the full path. */
|
|
88
90
|
executionVerified?: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* #394: true when an engine-built evaluator's key lookup reproduced every radiance of the template's own full execution; its
|
|
93
|
+
* sites are then planned as key taps and their radiances taken from the lookup instead of the evaluator (tabulated keys, chart
|
|
94
|
+
* materials, from `radiance`: per key, per spectral node, NaN until first needed).
|
|
95
|
+
*/
|
|
96
|
+
keyMode?: boolean;
|
|
97
|
+
radiance?: Float64Array[];
|
|
89
98
|
}
|
|
90
99
|
interface PlanConstants {
|
|
91
100
|
spectralCount: number;
|
|
@@ -131,7 +140,10 @@ interface PlanConstants {
|
|
|
131
140
|
sceneBindings: FullPlan["sceneBindings"];
|
|
132
141
|
}
|
|
133
142
|
interface LeanTap {
|
|
134
|
-
request
|
|
143
|
+
/** The provider request; absent for a key tap (#394), whose radiance comes from the evaluator's key lookup. */
|
|
144
|
+
request?: SceneRadianceEvaluationRequest;
|
|
145
|
+
/** #394: the key (chart material or lattice site) the request would read (key taps only). */
|
|
146
|
+
key?: number;
|
|
135
147
|
sourcePointImagePlaneMm: {
|
|
136
148
|
x: number;
|
|
137
149
|
y: number;
|
|
@@ -167,6 +179,8 @@ export interface LeanEnvironmentPlan {
|
|
|
167
179
|
}[];
|
|
168
180
|
/** Per group: the resolved PSF kernel weights. Present only with a sampled PSF. */
|
|
169
181
|
readonly kernels?: readonly (readonly number[])[];
|
|
182
|
+
/** #394: true when the taps are key taps (no requests). */
|
|
183
|
+
readonly keyTaps?: true;
|
|
170
184
|
}
|
|
171
185
|
export declare function isLeanEnvironmentPlan(plan: unknown): plan is LeanEnvironmentPlan;
|
|
172
186
|
export interface NativeEnvironmentLeanFrame {
|
|
@@ -197,9 +211,25 @@ export interface NativeEnvironmentLeanExecutor {
|
|
|
197
211
|
* caller then plans and executes this site in full. `fullPlan` builds the site's full plan without callbacks;
|
|
198
212
|
* it is called only when a sampled-PSF check fails and the full PSF quadrature must decide.
|
|
199
213
|
*/
|
|
200
|
-
execute(plan: LeanEnvironmentPlan, evaluate: EnvironmentRadianceEvaluator, aperture: EnvironmentApertureRadianceEvaluator | undefined, fullPlan: () => FullPlan, internalEvaluator?: boolean): SensorEqeTemporalPhotoSignal | undefined;
|
|
214
|
+
execute(plan: LeanEnvironmentPlan, evaluate: EnvironmentRadianceEvaluator, aperture: EnvironmentApertureRadianceEvaluator | undefined, fullPlan: () => FullPlan, internalEvaluator?: boolean, countEvaluations?: (count: number) => void): SensorEqeTemporalPhotoSignal | undefined;
|
|
201
215
|
}
|
|
202
|
-
|
|
216
|
+
/**
|
|
217
|
+
* Creates the per-event lean executor. With `access` (#394: the key lookup of an engine-built evaluator), a template whose own
|
|
218
|
+
* site verifies it plans its sites as key taps and takes their radiances from the lookup.
|
|
219
|
+
*/
|
|
220
|
+
export declare function createNativeEnvironmentLeanExecutor(frame: NativeEnvironmentLeanFrame, access?: InternalRadianceAccess): NativeEnvironmentLeanExecutor;
|
|
221
|
+
/** #394: the sensor stage of one CFA phase for given expected photo electrons, verified against a full site computation. */
|
|
222
|
+
export interface NativeSiteReadout {
|
|
223
|
+
/** The site's RAW outputs for expected photo electrons `photoE` over a local exposure of `duration` seconds; undefined where a check of the full path would throw. */
|
|
224
|
+
compute(photoE: number, duration: number, x: number, y: number, index: number, seed: number): NativeRawLeanOutput | undefined;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* #394: a sensor template from one fully parsed and computed site (`calculateNativeRawSite`), as the lean sensor stage builds
|
|
228
|
+
* its templates; undefined unless the lean arithmetic reproduces that site's dark and total charge, both charge realizations,
|
|
229
|
+
* its codes and its flags bit for bit. Its `compute` then gives `calculateNativeRawSite`'s outputs for any site of the same
|
|
230
|
+
* readout inputs (same CFA channel, dark current, capacity, readout regime and fixed-pattern profile).
|
|
231
|
+
*/
|
|
232
|
+
export declare function createVerifiedNativeSiteReadout(parsed: SensorRawProducerSiteInput, result: ReturnType<typeof calculateNativeRawSite>, whiteCode: number, photo: SensorEqeTemporalPhotoSignal, x: number, y: number, index: number, seed: number): NativeSiteReadout | undefined;
|
|
203
233
|
/**
|
|
204
234
|
* Lean sensor stage for createNativeEnvironmentRawTask.
|
|
205
235
|
*
|
|
@@ -30,7 +30,7 @@ const REDUCTION_TOLERANCE = 1e-10;
|
|
|
30
30
|
/** Disables the lean executor process-wide (internal; tests compare both paths). */
|
|
31
31
|
let leanDisabled = false;
|
|
32
32
|
/** Internal counters of lean work (tests assert the lean path actually runs). Not exported from the package. */
|
|
33
|
-
export const nativeEnvironmentLeanStatistics = { plannedSites: 0, executedSites: 0, sensorSites: 0, psfFallbacks: 0 };
|
|
33
|
+
export const nativeEnvironmentLeanStatistics = { plannedSites: 0, executedSites: 0, sensorSites: 0, psfFallbacks: 0, keySites: 0 };
|
|
34
34
|
export function setNativeEnvironmentLeanExecutionEnabled(enabled) { leanDisabled = !enabled; }
|
|
35
35
|
function isPlainRecord(value) {
|
|
36
36
|
const prototype = Object.getPrototypeOf(value);
|
|
@@ -137,8 +137,12 @@ function siteOf(environment) {
|
|
|
137
137
|
const site = plainChild(plainChild(plainChild(environment, "sensor"), "spatialSampling"), "site");
|
|
138
138
|
return site;
|
|
139
139
|
}
|
|
140
|
-
/**
|
|
141
|
-
|
|
140
|
+
/**
|
|
141
|
+
* Plans the site-dependent parts with the full planner's arithmetic. Returns undefined on any doubt, and the caller plans in full.
|
|
142
|
+
* With `access` (#394: a verified key-mode template, no pupil or lens mapping), each tap keeps the key its request would read
|
|
143
|
+
* instead of the request.
|
|
144
|
+
*/
|
|
145
|
+
function geometry(t, k, frame, environment, index, access) {
|
|
142
146
|
try {
|
|
143
147
|
const x = index % frame.nativeWidth, y = Math.floor(index / frame.nativeWidth), site = siteOf(environment);
|
|
144
148
|
// Object.is: a -0 coordinate would flow into the full path's outputs, so it takes the full path.
|
|
@@ -233,14 +237,22 @@ function geometry(t, k, frame, environment, index) {
|
|
|
233
237
|
const displacement = { pitch: omega.pitch * time, yaw: omega.yaw * time, roll: omega.roll * time };
|
|
234
238
|
const taps = [];
|
|
235
239
|
for (let gi = 0; gi < S * P; gi++) {
|
|
240
|
+
// #394: without a PSF a key tap depends only on its spatial node (same point, direction, field and key at
|
|
241
|
+
// every spectral node), so groups after the first spectral node share the first one's tap.
|
|
242
|
+
if (access !== undefined && destinationFields !== undefined && gi >= P) {
|
|
243
|
+
taps.push(taps[gi % P]);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
236
246
|
const s = Math.floor(gi / P), points = destinationFields === undefined ? tapPoints[gi] : undefined;
|
|
237
247
|
const tapCount = points === undefined ? 1 : points.length;
|
|
238
248
|
for (let pi = 0; pi < tapCount; pi++) {
|
|
239
|
-
const point = points === undefined ? sources[gi % P] : points[pi], sampleId = `${id}-t${ti}-n${gi}-p${pi}
|
|
249
|
+
const point = points === undefined ? sources[gi % P] : points[pi], sampleId = access === undefined ? `${id}-t${ti}-n${gi}-p${pi}` : "";
|
|
240
250
|
const apertureRays = pupil === undefined ? undefined : sensorApertureRayValues({ sourcePointNativeSensorMm: point, focalLengthMm,
|
|
241
251
|
nominalFNumber, angularVelocityRadPerSec: omega, timeSecondsFromOpeningReference: time }, pupil, k.parsedFocus).rays;
|
|
242
|
-
let request, imagePointMm, distortionAreaFactor;
|
|
252
|
+
let request, key, imagePointMm, distortionAreaFactor;
|
|
243
253
|
if (k.lensMapping !== undefined) {
|
|
254
|
+
if (access !== undefined)
|
|
255
|
+
return undefined;
|
|
244
256
|
// The full planner's own mapped query, with the same input; only its result envelope is not kept.
|
|
245
257
|
const query = calculateMappedSensorEnvironmentRadianceQuery({ ...motion, sourcePointNativeSensorMm: point, focalLengthMm, focus: k.focus,
|
|
246
258
|
timeSecondsFromOpeningReference: time, request: { schemaVersion: "0.1.0", sampleId, providerProfileId: base.providerProfileId, sceneId: base.sceneId,
|
|
@@ -272,15 +284,26 @@ function geometry(t, k, frame, environment, index) {
|
|
|
272
284
|
const norm = Math.hypot(direction.x, direction.y, direction.z);
|
|
273
285
|
if (!Number.isFinite(norm) || Math.abs(norm - 1) > 1e-9)
|
|
274
286
|
return undefined;
|
|
275
|
-
request
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
287
|
+
// #394: the key the evaluator would read for this request (undefined where it would throw: the full path decides).
|
|
288
|
+
if (access !== undefined) {
|
|
289
|
+
key = access.keyOfDirection(direction);
|
|
290
|
+
if (key === undefined)
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
else
|
|
294
|
+
request = Object.freeze({ schemaVersion: "0.1.0", sampleId, providerProfileId: base.providerProfileId,
|
|
295
|
+
sceneId: base.sceneId, illuminationProfileId: base.illuminationProfileId, materialResponseProfileId: base.materialResponseProfileId,
|
|
296
|
+
target: Object.freeze({ kind: "environment-direction", outgoingDirectionUnitVector: Object.freeze(direction) }),
|
|
297
|
+
timeSecondsFromExposureStart: time, wavelengthNanometers: k.wavelengths[s],
|
|
298
|
+
wavelengthBasis: base.wavelengthBasis });
|
|
280
299
|
}
|
|
281
300
|
const f = destinationFields === undefined ? tapFields[gi][pi] : destinationFields[gi % P];
|
|
282
301
|
// Built without object spread (cheaper per tap); optional fields are added only when present.
|
|
283
|
-
const leanTap = {
|
|
302
|
+
const leanTap = { sourcePointImagePlaneMm: imagePointMm, fieldThroughput: f.value, fieldFactor: f.factor, fieldValid: f.valid };
|
|
303
|
+
if (request !== undefined)
|
|
304
|
+
leanTap.request = request;
|
|
305
|
+
else
|
|
306
|
+
leanTap.key = key;
|
|
284
307
|
if (apertureRays !== undefined)
|
|
285
308
|
leanTap.apertureRays = apertureRays;
|
|
286
309
|
if (distortionAreaFactor !== undefined)
|
|
@@ -291,7 +314,7 @@ function geometry(t, k, frame, environment, index) {
|
|
|
291
314
|
instants.push({ time, taps });
|
|
292
315
|
}
|
|
293
316
|
return { lean: true, template: t, index, owned: environment, count: k.count, site: { x, y }, start, end, duration, instants,
|
|
294
|
-
...(k.psf === undefined ? {} : { kernels }) };
|
|
317
|
+
...(k.psf === undefined ? {} : { kernels }), ...(access === undefined ? {} : { keyTaps: true }) };
|
|
295
318
|
}
|
|
296
319
|
catch {
|
|
297
320
|
return undefined;
|
|
@@ -610,7 +633,45 @@ function executionConstants(k, environment, plan, result, self) {
|
|
|
610
633
|
const mine = photo(k, c, values, environment, full.site, full.startOffsetSecondsFromOpeningReference, full.endOffsetSecondsFromOpeningReference, full.localExposureDurationSeconds, plan.instants.map(i => i.time));
|
|
611
634
|
return mine && sameData(mine, full, new WeakMap()) ? c : undefined;
|
|
612
635
|
}
|
|
613
|
-
|
|
636
|
+
/** #394: the evaluator's radiance for a key at spectral node s (tabulated keys from the template's table, computed once). */
|
|
637
|
+
function keyRadiance(t, k, access, key, s) {
|
|
638
|
+
// Untabulated keys (lattice sites) are read from the evaluator's own lookup on each use.
|
|
639
|
+
if (!access.tabulated)
|
|
640
|
+
return access.radianceOfKey(key, k.wavelengths[s]);
|
|
641
|
+
const table = t.radiance, row = table[key] ??= new Float64Array(k.spectralCount).fill(NaN);
|
|
642
|
+
let value = row[s];
|
|
643
|
+
if (Number.isNaN(value)) {
|
|
644
|
+
value = access.radianceOfKey(key, k.wavelengths[s]);
|
|
645
|
+
row[s] = value;
|
|
646
|
+
}
|
|
647
|
+
return value;
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* #394: true when the key taps of the template's own site give, tap by tap in callback order, exactly the radiance of each
|
|
651
|
+
* evaluation of its full execution. Only then do its sites take radiances from the key lookup.
|
|
652
|
+
*/
|
|
653
|
+
function keyModeReproduced(t, k, access, self, result) {
|
|
654
|
+
if (!self || self.keyTaps !== true || k.pupilCount !== 1 || self.instants.length !== result.value.instants.length)
|
|
655
|
+
return false;
|
|
656
|
+
const S = k.spectralCount, P = k.spatialCount;
|
|
657
|
+
for (let ti = 0; ti < self.instants.length; ti++) {
|
|
658
|
+
const evaluations = result.value.instants[ti].evaluations, taps = self.instants[ti].taps;
|
|
659
|
+
if (evaluations.length !== taps.length)
|
|
660
|
+
return false;
|
|
661
|
+
for (let gi = 0; gi < S * P; gi++)
|
|
662
|
+
for (let i = k.tapOffsets[gi]; i < k.tapOffsets[gi + 1]; i++) {
|
|
663
|
+
const key = taps[i].key;
|
|
664
|
+
if (key === undefined || !Object.is(keyRadiance(t, k, access, key, Math.floor(gi / P)), evaluations[i].result.spectralRadianceWattsPerSquareMeterSteradianNanometer))
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return true;
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Creates the per-event lean executor. With `access` (#394: the key lookup of an engine-built evaluator), a template whose own
|
|
672
|
+
* site verifies it plans its sites as key taps and takes their radiances from the lookup.
|
|
673
|
+
*/
|
|
674
|
+
export function createNativeEnvironmentLeanExecutor(frame, access) {
|
|
614
675
|
const templates = [];
|
|
615
676
|
const candidates = new Map();
|
|
616
677
|
const memo = new WeakMap();
|
|
@@ -651,7 +712,7 @@ export function createNativeEnvironmentLeanExecutor(frame) {
|
|
|
651
712
|
return templates.length < MAXIMUM_TEMPLATES ? "candidate" : undefined;
|
|
652
713
|
if (t.disabled || !t.consts || t.executionVerified === false)
|
|
653
714
|
return undefined;
|
|
654
|
-
const leanPlan = geometry(t, t.consts, frame, environment, index);
|
|
715
|
+
const leanPlan = geometry(t, t.consts, frame, environment, index, t.keyMode === true ? access : undefined);
|
|
655
716
|
if (leanPlan)
|
|
656
717
|
nativeEnvironmentLeanStatistics.plannedSites++;
|
|
657
718
|
return leanPlan;
|
|
@@ -688,19 +749,81 @@ export function createNativeEnvironmentLeanExecutor(frame) {
|
|
|
688
749
|
return;
|
|
689
750
|
t.execution = c;
|
|
690
751
|
t.executionVerified = true;
|
|
752
|
+
// #394: key taps need no pupil and no lens mapping (the evaluator then reads one plane point per tap).
|
|
753
|
+
if (access !== undefined && t.consts.lensMapping === undefined && t.environment.pupil === undefined) {
|
|
754
|
+
t.radiance = [];
|
|
755
|
+
t.keyMode = keyModeReproduced(t, t.consts, access, geometry(t, t.consts, frame, t.environment, index, access), result);
|
|
756
|
+
if (!t.keyMode)
|
|
757
|
+
delete t.radiance;
|
|
758
|
+
}
|
|
691
759
|
}
|
|
692
760
|
catch {
|
|
693
761
|
// Unverified: sites of this template run the full path.
|
|
694
762
|
}
|
|
695
763
|
},
|
|
696
|
-
execute(plan, evaluate, aperture, fullPlan, internalEvaluator = false) {
|
|
764
|
+
execute(plan, evaluate, aperture, fullPlan, internalEvaluator = false, countEvaluations) {
|
|
697
765
|
const t = plan.template, c = t.execution, k = t.consts;
|
|
698
766
|
if (t.executionVerified !== true || !c || !k)
|
|
699
767
|
return undefined;
|
|
700
768
|
const environment = plan.owned, optics = environment.optics;
|
|
701
769
|
const S = k.spectralCount, P = k.spatialCount, kernels = plan.kernels;
|
|
770
|
+
if (plan.keyTaps === true) {
|
|
771
|
+
// #394: every tap's radiance from the evaluator's key lookup, with tapIrradiance's arithmetic. A tap that would
|
|
772
|
+
// need the full irradiance function returns undefined before anything is counted: the caller runs the site in full.
|
|
773
|
+
if (t.keyMode !== true || !access || !countEvaluations)
|
|
774
|
+
return undefined;
|
|
775
|
+
const values = [], times = [], instantTaps = [];
|
|
776
|
+
for (const instant of plan.instants) {
|
|
777
|
+
const v = new Float64Array(S * P), tapValues = [];
|
|
778
|
+
if (kernels === undefined) {
|
|
779
|
+
// One tap per group.
|
|
780
|
+
for (let gi = 0; gi < S * P; gi++) {
|
|
781
|
+
const tap = instant.taps[gi], s = Math.floor(gi / P);
|
|
782
|
+
const irradiance = keyRadiance(t, k, access, tap.key, s) * (c.acceptance[s] * tap.fieldFactor);
|
|
783
|
+
if (!tap.fieldValid || !Number.isFinite(irradiance) || irradiance < 0)
|
|
784
|
+
return undefined;
|
|
785
|
+
let integrated = 0;
|
|
786
|
+
integrated += irradiance * 1;
|
|
787
|
+
v[gi] = integrated;
|
|
788
|
+
}
|
|
789
|
+
values.push(v);
|
|
790
|
+
times.push(instant.time);
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
for (let gi = 0; gi < S * P; gi++) {
|
|
794
|
+
const first = k.tapOffsets[gi], s = Math.floor(gi / P), groupValues = new Float64Array(k.tapOffsets[gi + 1] - first);
|
|
795
|
+
for (let pi = 0; pi < groupValues.length; pi++) {
|
|
796
|
+
const tap = instant.taps[first + pi];
|
|
797
|
+
const irradiance = keyRadiance(t, k, access, tap.key, s) * (c.acceptance[s] * tap.fieldFactor);
|
|
798
|
+
if (!tap.fieldValid || !Number.isFinite(irradiance) || irradiance < 0)
|
|
799
|
+
return undefined;
|
|
800
|
+
let integrated = 0;
|
|
801
|
+
integrated += irradiance * 1;
|
|
802
|
+
groupValues[pi] = integrated;
|
|
803
|
+
}
|
|
804
|
+
tapValues.push(groupValues);
|
|
805
|
+
v[gi] = kernels === undefined ? groupValues[0] : psfSum(kernels[gi], groupValues);
|
|
806
|
+
}
|
|
807
|
+
values.push(v);
|
|
808
|
+
instantTaps.push(tapValues);
|
|
809
|
+
times.push(instant.time);
|
|
810
|
+
}
|
|
811
|
+
// The evaluations the full path would count (and its abort check), then the PSF checks after each instant's taps.
|
|
812
|
+
countEvaluations(plan.count);
|
|
813
|
+
let full;
|
|
814
|
+
if (kernels !== undefined)
|
|
815
|
+
for (let ti = 0; ti < values.length; ti++) {
|
|
816
|
+
if (!psfChecksPass(k, c, instantTaps[ti], values[ti]))
|
|
817
|
+
values[ti] = fullPsfInstant(full ??= fullPlan(), ti, instantTaps[ti]);
|
|
818
|
+
}
|
|
819
|
+
nativeEnvironmentLeanStatistics.executedSites++;
|
|
820
|
+
nativeEnvironmentLeanStatistics.keySites++;
|
|
821
|
+
return photo(k, c, values, environment, plan.site, plan.start, plan.end, plan.duration, times) ??
|
|
822
|
+
createSensorEqeTemporalPhotoSignal(fullPhotoInput(k, values, environment, times)).value.photoSignal;
|
|
823
|
+
}
|
|
702
824
|
/** One tap: its callbacks in order, each result validated, the pupil sum, then the distortion area factor once. */
|
|
703
|
-
const tapIrradiance = (
|
|
825
|
+
const tapIrradiance = (leanTap, s) => {
|
|
826
|
+
const tap = leanTap;
|
|
704
827
|
let integrated = 0;
|
|
705
828
|
const support = tap.apertureRays ?? [undefined];
|
|
706
829
|
for (const ray of support) {
|
|
@@ -888,6 +1011,42 @@ function chargeTotal(photoE, darkE) {
|
|
|
888
1011
|
}
|
|
889
1012
|
return sum;
|
|
890
1013
|
}
|
|
1014
|
+
/**
|
|
1015
|
+
* #394: a sensor template from one fully parsed and computed site (`calculateNativeRawSite`), as the lean sensor stage builds
|
|
1016
|
+
* its templates; undefined unless the lean arithmetic reproduces that site's dark and total charge, both charge realizations,
|
|
1017
|
+
* its codes and its flags bit for bit. Its `compute` then gives `calculateNativeRawSite`'s outputs for any site of the same
|
|
1018
|
+
* readout inputs (same CFA channel, dark current, capacity, readout regime and fixed-pattern profile).
|
|
1019
|
+
*/
|
|
1020
|
+
export function createVerifiedNativeSiteReadout(parsed, result, whiteCode, photo, x, y, index, seed) {
|
|
1021
|
+
try {
|
|
1022
|
+
const readout = resolveSensorReadoutRegime({ profile: parsed.readoutProfile, regimeId: parsed.regimeId });
|
|
1023
|
+
const t = { darkRate: parsed.charge.darkCharge.darkCurrentElectronsPerSecond,
|
|
1024
|
+
capacity: result.capacity.value.physicalChargeCapacityElectrons, readout, pattern: parsed.fixedPatternNoise, whiteCode };
|
|
1025
|
+
const darkE = t.darkRate * photo.localExposureDurationSeconds, total = chargeTotal(photo.expectedGeneratedElectronCount, darkE);
|
|
1026
|
+
const r = result.readout.value, realization = result.realization.value, out = leanSensorSite(t, { photo, darkE, total, x, y }, index, seed);
|
|
1027
|
+
if (!out || !Object.is(darkE, parsed.charge.darkCharge.expectedDarkElectronCount) || !Object.is(total, result.accumulatedCharge.value.totalExpectedStoredElectronCount) ||
|
|
1028
|
+
!Object.is(out.photoRealized, realization.photoRealizedElectronCount) || !Object.is(out.darkRealized, realization.darkRealizedElectronCount) ||
|
|
1029
|
+
realization.additionalComponents.length !== 0 || out.rawCode !== r.rawCode || out.blackLevelCode !== r.blackLevelCode || out.digitalSaturationCode !== r.digitalSaturationCode ||
|
|
1030
|
+
out.flags !== (Number(r.physicalScalarSaturationApplied) | Number(r.preAdcSaturationApplied) << 1 | Number(r.digitalSaturationApplied) << 2))
|
|
1031
|
+
return undefined;
|
|
1032
|
+
return {
|
|
1033
|
+
compute(photoE, duration, sx, sy, siteIndex, siteSeed) {
|
|
1034
|
+
// The parse-phase predicates of the full path (claim()): finite non-negative dark and total charge.
|
|
1035
|
+
const siteDark = t.darkRate * duration;
|
|
1036
|
+
if (!Number.isFinite(photoE) || photoE < 0 || !Number.isFinite(siteDark) || siteDark < 0)
|
|
1037
|
+
return undefined;
|
|
1038
|
+
const siteTotal = chargeTotal(photoE, siteDark);
|
|
1039
|
+
if (!Number.isFinite(siteTotal) || siteTotal < 0)
|
|
1040
|
+
return undefined;
|
|
1041
|
+
const o = leanSensorSite(t, { photo: { expectedGeneratedElectronCount: photoE }, darkE: siteDark, total: siteTotal, x: sx, y: sy }, siteIndex, siteSeed);
|
|
1042
|
+
return o && { rawCode: o.rawCode, blackLevelCode: o.blackLevelCode, digitalSaturationCode: o.digitalSaturationCode, flags: o.flags, whiteCode: o.whiteCode };
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
catch {
|
|
1047
|
+
return undefined;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
891
1050
|
export function createNativeEnvironmentSensorLean(frame, producerFrame, nativeWindow) {
|
|
892
1051
|
const enabled = !leanDisabled && nativeWindow !== undefined && frame.identityBinding && frame.repeatWidth > 0 && frame.repeatHeight > 0;
|
|
893
1052
|
const templates = [], handles = new WeakMap(), memo = new WeakMap();
|