@meimu/mutracer 0.2.0 → 0.3.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.
Files changed (211) hide show
  1. package/README.md +3 -0
  2. package/dist/assets/bvh.worker.js +1 -1
  3. package/dist/mutracer.umd.js +8949 -7915
  4. package/docs/api.md +42 -21
  5. package/docs/caustics.md +80 -0
  6. package/docs/materials.md +5 -4
  7. package/docs/progressive-rendering.md +50 -9
  8. package/docs/technical/continuous-spectrum.md +93 -0
  9. package/docs/updates.md +3 -1
  10. package/docs/viewer-checklist.md +65 -0
  11. package/docs/viewer.md +7 -5
  12. package/lib/Display.js +1 -1
  13. package/lib/Display.js.map +1 -1
  14. package/lib/Engine.d.ts +2 -0
  15. package/lib/Engine.js +47 -12
  16. package/lib/Engine.js.map +1 -1
  17. package/lib/Render.d.ts +61 -13
  18. package/lib/Render.js +169 -19
  19. package/lib/Render.js.map +1 -1
  20. package/lib/RenderLoop.d.ts +7 -3
  21. package/lib/RenderLoop.js +68 -44
  22. package/lib/RenderLoop.js.map +1 -1
  23. package/lib/main.d.ts +1 -1
  24. package/lib/main.js.map +1 -1
  25. package/lib/oidn.d.ts +1 -1
  26. package/lib/oidnBundledWeights.cdn.d.ts +2 -2
  27. package/lib/renderer/GPUTraceQueue.d.ts +2 -1
  28. package/lib/renderer/GPUTraceQueue.js +3 -1
  29. package/lib/renderer/GPUTraceQueue.js.map +1 -1
  30. package/lib/renderer/RenderingPipeline.d.ts +4 -0
  31. package/lib/renderer/RenderingPipeline.js.map +1 -1
  32. package/lib/renderer/WGPUCausticLightPass.d.ts +48 -0
  33. package/lib/renderer/WGPUCausticLightPass.js +217 -0
  34. package/lib/renderer/WGPUCausticLightPass.js.map +1 -0
  35. package/lib/renderer/WGPUEnvMapDistribution.js +18 -18
  36. package/lib/renderer/WGPUFullQuadPass.js +3 -3
  37. package/lib/renderer/WGPUPhotonMap.d.ts +37 -0
  38. package/lib/renderer/WGPUPhotonMap.js +191 -0
  39. package/lib/renderer/WGPUPhotonMap.js.map +1 -0
  40. package/lib/renderer/WGPURayTraceOutput.d.ts +13 -1
  41. package/lib/renderer/WGPURayTraceOutput.js +122 -28
  42. package/lib/renderer/WGPURayTraceOutput.js.map +1 -1
  43. package/lib/renderer/WGPURayTracePass.d.ts +48 -17
  44. package/lib/renderer/WGPURayTracePass.js +538 -340
  45. package/lib/renderer/WGPURayTracePass.js.map +1 -1
  46. package/lib/renderer/WGPURenderingPipeline.js +83 -47
  47. package/lib/renderer/WGPURenderingPipeline.js.map +1 -1
  48. package/lib/renderer/WebGLFullQuadPass.d.ts +2 -1
  49. package/lib/renderer/WebGLFullQuadPass.js +4 -0
  50. package/lib/renderer/WebGLFullQuadPass.js.map +1 -1
  51. package/lib/renderer/WebGLJNLMDenoiser.js +66 -66
  52. package/lib/renderer/WebGLRayTraceOutputPass.js +4 -4
  53. package/lib/renderer/WebGLRayTracePass.d.ts +6 -1
  54. package/lib/renderer/WebGLRayTracePass.js +43 -6
  55. package/lib/renderer/WebGLRayTracePass.js.map +1 -1
  56. package/lib/renderer/WebGLRenderingPipeline.js +10 -14
  57. package/lib/renderer/WebGLRenderingPipeline.js.map +1 -1
  58. package/lib/renderer/WebGLSkyBaker.js +60 -60
  59. package/lib/renderer/accumulationTarget.d.ts +2 -1
  60. package/lib/renderer/accumulationTarget.js.map +1 -1
  61. package/lib/renderer/causticDiagnostics.d.ts +31 -0
  62. package/lib/renderer/causticDiagnostics.js +5 -0
  63. package/lib/renderer/causticDiagnostics.js.map +1 -0
  64. package/lib/renderer/causticEmission.d.ts +10 -0
  65. package/lib/renderer/causticEmission.js +40 -0
  66. package/lib/renderer/causticEmission.js.map +1 -0
  67. package/lib/renderer/causticUniform.d.ts +12 -0
  68. package/lib/renderer/causticUniform.js +22 -0
  69. package/lib/renderer/causticUniform.js.map +1 -0
  70. package/lib/renderer/caustics.d.ts +16 -0
  71. package/lib/renderer/caustics.js +93 -0
  72. package/lib/renderer/caustics.js.map +1 -0
  73. package/lib/renderer/glsl/aovDisplay.frag.js +5 -5
  74. package/lib/renderer/glsl/aovReflectance.glsl.js +53 -53
  75. package/lib/renderer/glsl/aovReflectanceSamples.glsl.js +1 -1
  76. package/lib/renderer/glsl/aovTrace.frag.js +33 -33
  77. package/lib/renderer/glsl/chunks/cameraRay.glsl.js +9 -9
  78. package/lib/renderer/glsl/chunks/emissiveLight.glsl.d.ts +2 -0
  79. package/lib/renderer/glsl/chunks/emissiveLight.glsl.js +35 -0
  80. package/lib/renderer/glsl/chunks/emissiveLight.glsl.js.map +1 -0
  81. package/lib/renderer/glsl/chunks/envMap.glsl.js +73 -73
  82. package/lib/renderer/glsl/chunks/intersect.glsl.js +426 -428
  83. package/lib/renderer/glsl/chunks/intersect.glsl.js.map +1 -1
  84. package/lib/renderer/glsl/chunks/materialBuffer.glsl.d.ts +1 -0
  85. package/lib/renderer/glsl/chunks/materialBuffer.glsl.js +156 -153
  86. package/lib/renderer/glsl/chunks/materialBuffer.glsl.js.map +1 -1
  87. package/lib/renderer/glsl/chunks/openPbrBsdf.glsl.js +771 -771
  88. package/lib/renderer/glsl/chunks/rand.glsl.js +3 -3
  89. package/lib/renderer/glsl/chunks/random.glsl.js +6 -6
  90. package/lib/renderer/glsl/chunks/rayTraceCore.glsl.d.ts +1 -0
  91. package/lib/renderer/glsl/chunks/rayTraceCore.glsl.js +68 -63
  92. package/lib/renderer/glsl/chunks/rayTraceCore.glsl.js.map +1 -1
  93. package/lib/renderer/glsl/chunks/sampleLight.glsl.js +94 -76
  94. package/lib/renderer/glsl/chunks/sampleLight.glsl.js.map +1 -1
  95. package/lib/renderer/glsl/chunks/sampleMaterial.glsl.d.ts +1 -0
  96. package/lib/renderer/glsl/chunks/sampleMaterial.glsl.js +140 -117
  97. package/lib/renderer/glsl/chunks/sampleMaterial.glsl.js.map +1 -1
  98. package/lib/renderer/glsl/chunks/sampleShadowCatcher.glsl.js +14 -14
  99. package/lib/renderer/glsl/chunks/sampling.glsl.js +89 -92
  100. package/lib/renderer/glsl/chunks/sampling.glsl.js.map +1 -1
  101. package/lib/renderer/glsl/chunks/sobol.glsl.js +79 -79
  102. package/lib/renderer/glsl/chunks/textureLinear.glsl.js +5 -5
  103. package/lib/renderer/glsl/chunks/uvProjection.glsl.js +11 -11
  104. package/lib/renderer/glsl/depthQuery.frag.js +2 -2
  105. package/lib/renderer/glsl/normalizeRayTraceOutput.frag.js +10 -10
  106. package/lib/renderer/glsl/rayTrace.frag.d.ts +1 -1
  107. package/lib/renderer/glsl/rayTrace.frag.js +260 -237
  108. package/lib/renderer/glsl/rayTrace.frag.js.map +1 -1
  109. package/lib/renderer/glsl/shadowCatcher.frag.js +26 -26
  110. package/lib/renderer/glsl/shadowCatcher.frag.js.map +1 -1
  111. package/lib/renderer/glsl/webglPipeline.frag.js +59 -59
  112. package/lib/renderer/lightSampling.d.ts +3 -2
  113. package/lib/renderer/lightSampling.js +14 -8
  114. package/lib/renderer/lightSampling.js.map +1 -1
  115. package/lib/renderer/progressiveScheduler.js +4 -3
  116. package/lib/renderer/progressiveScheduler.js.map +1 -1
  117. package/lib/renderer/renderPipeline.d.ts +1 -0
  118. package/lib/renderer/renderPipeline.js.map +1 -1
  119. package/lib/renderer/researchTransport.d.ts +31 -0
  120. package/lib/renderer/researchTransport.js +5 -0
  121. package/lib/renderer/researchTransport.js.map +1 -0
  122. package/lib/renderer/sceneLayoutSchema.d.ts +1 -0
  123. package/lib/renderer/sceneLayoutSchema.js +2 -2
  124. package/lib/renderer/sceneLayoutSchema.js.map +1 -1
  125. package/lib/renderer/scratchSampling.js +6 -6
  126. package/lib/renderer/spectrum.d.ts +21 -0
  127. package/lib/renderer/spectrum.js +154 -0
  128. package/lib/renderer/spectrum.js.map +1 -0
  129. package/lib/renderer/webglLightSampling.d.ts +39 -0
  130. package/lib/renderer/webglLightSampling.js +15 -0
  131. package/lib/renderer/webglLightSampling.js.map +1 -0
  132. package/lib/renderer/wgpuBindlessSource.js +3 -3
  133. package/lib/renderer/wgpuShaderSource.d.ts +7 -0
  134. package/lib/renderer/wgpuShaderSource.js +2 -0
  135. package/lib/renderer/wgpuShaderSource.js.map +1 -1
  136. package/lib/renderer/wgsl/aovDisplay.wgsl.js +9 -9
  137. package/lib/renderer/wgsl/aovReflectance.wgsl.js +55 -55
  138. package/lib/renderer/wgsl/aovReflectanceSamples.wgsl.js +1 -1
  139. package/lib/renderer/wgsl/aovResolve.wgsl.js +2 -2
  140. package/lib/renderer/wgsl/aovSnapshot.wgsl.js +1 -1
  141. package/lib/renderer/wgsl/aovTrace.wgsl.js +33 -33
  142. package/lib/renderer/wgsl/bloom.wgsl.js +18 -18
  143. package/lib/renderer/wgsl/causticLight.compute.d.ts +6 -0
  144. package/lib/renderer/wgsl/causticLight.compute.js +537 -0
  145. package/lib/renderer/wgsl/causticLight.compute.js.map +1 -0
  146. package/lib/renderer/wgsl/chunks/background.wgsl.d.ts +2 -0
  147. package/lib/renderer/wgsl/chunks/background.wgsl.js +102 -0
  148. package/lib/renderer/wgsl/chunks/background.wgsl.js.map +1 -0
  149. package/lib/renderer/wgsl/chunks/causticAdjoint.wgsl.d.ts +2 -0
  150. package/lib/renderer/wgsl/chunks/causticAdjoint.wgsl.js +12 -0
  151. package/lib/renderer/wgsl/chunks/causticAdjoint.wgsl.js.map +1 -0
  152. package/lib/renderer/wgsl/chunks/causticAtomic.wgsl.d.ts +2 -0
  153. package/lib/renderer/wgsl/chunks/causticAtomic.wgsl.js +16 -0
  154. package/lib/renderer/wgsl/chunks/causticAtomic.wgsl.js.map +1 -0
  155. package/lib/renderer/wgsl/chunks/caustics.wgsl.d.ts +1 -0
  156. package/lib/renderer/wgsl/chunks/caustics.wgsl.js +62 -0
  157. package/lib/renderer/wgsl/chunks/caustics.wgsl.js.map +1 -0
  158. package/lib/renderer/wgsl/chunks/common.wgsl.js +4 -4
  159. package/lib/renderer/wgsl/chunks/envMap.wgsl.js +37 -37
  160. package/lib/renderer/wgsl/chunks/intersect.wgsl.js +598 -592
  161. package/lib/renderer/wgsl/chunks/intersect.wgsl.js.map +1 -1
  162. package/lib/renderer/wgsl/chunks/intersectionAlpha.wgsl.js +14 -14
  163. package/lib/renderer/wgsl/chunks/materialBuffer.wgsl.d.ts +2 -1
  164. package/lib/renderer/wgsl/chunks/materialBuffer.wgsl.js +75 -73
  165. package/lib/renderer/wgsl/chunks/materialBuffer.wgsl.js.map +1 -1
  166. package/lib/renderer/wgsl/chunks/openPbrBsdf.wgsl.js +794 -794
  167. package/lib/renderer/wgsl/chunks/pdm_displacement.wgsl.js +139 -139
  168. package/lib/renderer/wgsl/chunks/rand.wgsl.js +3 -3
  169. package/lib/renderer/wgsl/chunks/rayTraceCore.wgsl.d.ts +2 -1
  170. package/lib/renderer/wgsl/chunks/rayTraceCore.wgsl.js +35 -25
  171. package/lib/renderer/wgsl/chunks/rayTraceCore.wgsl.js.map +1 -1
  172. package/lib/renderer/wgsl/chunks/sampleLight.wgsl.d.ts +1 -1
  173. package/lib/renderer/wgsl/chunks/sampleLight.wgsl.js +142 -138
  174. package/lib/renderer/wgsl/chunks/sampleLight.wgsl.js.map +1 -1
  175. package/lib/renderer/wgsl/chunks/sampleMaterial.wgsl.d.ts +1 -1
  176. package/lib/renderer/wgsl/chunks/sampleMaterial.wgsl.js +204 -187
  177. package/lib/renderer/wgsl/chunks/sampleMaterial.wgsl.js.map +1 -1
  178. package/lib/renderer/wgsl/chunks/sampleShadowCatcher.wgsl.d.ts +1 -1
  179. package/lib/renderer/wgsl/chunks/sampleShadowCatcher.wgsl.js +11 -11
  180. package/lib/renderer/wgsl/chunks/sampleShadowCatcher.wgsl.js.map +1 -1
  181. package/lib/renderer/wgsl/chunks/sampling.wgsl.d.ts +1 -1
  182. package/lib/renderer/wgsl/chunks/sampling.wgsl.js +89 -93
  183. package/lib/renderer/wgsl/chunks/sampling.wgsl.js.map +1 -1
  184. package/lib/renderer/wgsl/chunks/sobol.wgsl.js +83 -83
  185. package/lib/renderer/wgsl/chunks/uvProjection.wgsl.js +11 -11
  186. package/lib/renderer/wgsl/compose.frag.wgsl.js +98 -98
  187. package/lib/renderer/wgsl/customDenoise.wgsl.js +10 -10
  188. package/lib/renderer/wgsl/depthQuery.wgsl.js +2 -2
  189. package/lib/renderer/wgsl/jnlm.compute.js +74 -74
  190. package/lib/renderer/wgsl/offlineAdaptive.compute.js +25 -25
  191. package/lib/renderer/wgsl/oidnAuxiliary.wgsl.js +3 -3
  192. package/lib/renderer/wgsl/photonMap.compute.d.ts +29 -0
  193. package/lib/renderer/wgsl/photonMap.compute.js +352 -0
  194. package/lib/renderer/wgsl/photonMap.compute.js.map +1 -0
  195. package/lib/renderer/wgsl/proceduralSky.compute.js +64 -64
  196. package/lib/renderer/wgsl/rayTrace.compute.d.ts +9 -1
  197. package/lib/renderer/wgsl/rayTrace.compute.js +448 -522
  198. package/lib/renderer/wgsl/rayTrace.compute.js.map +1 -1
  199. package/lib/scene/MaterialState.d.ts +2 -2
  200. package/lib/scene/MaterialState.js +2 -2
  201. package/lib/scene/MaterialState.js.map +1 -1
  202. package/lib/settings/render.d.ts +11 -0
  203. package/lib/settings/render.js +1 -0
  204. package/lib/settings/render.js.map +1 -1
  205. package/lib/util/fromTHREE.js +2 -2
  206. package/lib/util/fromTHREE.js.map +1 -1
  207. package/lib/version.d.ts +1 -1
  208. package/lib/version.js +1 -1
  209. package/lib/version.js.map +1 -1
  210. package/llms.txt +2 -1
  211. package/package.json +3 -3
package/lib/Render.d.ts CHANGED
@@ -17,8 +17,8 @@ export type RenderStatus = 'preparing' | 'compiling' | 'converging' | 'denoising
17
17
  /** Execution overrides for one refine call; accumulated samples are retained. */
18
18
  export interface RefineOptions {
19
19
  /** Batch several samples in this call. Default this render's batchSamples. */ batchSamples?: boolean;
20
- /** Positive finite WebGPU queue budget in milliseconds; capped by timeBudgetMs, ignored on WebGL. Default timeBudgetMs. */ targetBatchTimeMs?: number;
21
- /** Leave the displayed image unchanged, for calls between display frames; the next presentation or pixel read updates it once with every new sample. Ignored on WebGL. Default false. */ deferResolve?: boolean;
20
+ /** @deprecated Use refineUntil; this option will be removed in a later release. Positive finite WebGPU queue budget in milliseconds; capped by timeBudgetMs, ignored on WebGL. Default timeBudgetMs. */ targetBatchTimeMs?: number;
21
+ /** @deprecated Use refineUntil; this option will be removed in a later release. Leave the displayed image unchanged, for calls between display frames; the next presentation or pixel read updates it once with every new sample. Ignored on WebGL. Default false. */ deferResolve?: boolean;
22
22
  }
23
23
  /** When a render filters its complete samples. Default converged. */
24
24
  export type DenoiseSchedule = 'converged' | 'progressive' | 'every-sample';
@@ -30,7 +30,7 @@ export interface Denoiser {
30
30
  width: number;
31
31
  height: number;
32
32
  }): Promise<void>;
33
- /** Write all output RGB, then resolve after submitting GPU work. Abort on input changes, restart, replacement or disposal; aborted results are discarded even if this resolves. Buffers are borrowed until settlement; do not retain or destroy them. */
33
+ /** Write all output RGB, then resolve after submitting GPU work. Abort on input changes, restart, replacement or disposal; aborted results are discarded even if this resolves. Settle within 10 seconds of an abort, or later denoising continues without waiting. Buffers are borrowed until settlement; do not retain or destroy them. */
34
34
  denoise(input: DenoiseInput, context: {
35
35
  signal: AbortSignal;
36
36
  schedule: DenoiseSchedule;
@@ -88,6 +88,21 @@ export type OIDNDenoiserOptions = OIDNSettings & ({
88
88
  });
89
89
  /** Assign a complete configuration, or null to disable denoising. The type selector is required. */
90
90
  export type DenoiserOptions = JNLMDenoiserOptions | OIDNDenoiserOptions | CustomDenoiserOptions;
91
+ /** Optional caustic estimators. All are disabled by default. Both fall back to path tracing when a smooth caster has stochastic blend coverage. */
92
+ export interface CausticSettings {
93
+ /** WebGPU progressive photon mapping; ignored on WebGL. Used alone, it can reduce denoising blotches on directly visible receivers, with noisier raw output. Default `false`. */
94
+ photonMapping?: boolean | {
95
+ /** Emitted photons per batch; 28 bytes per photon plus up to 13 MiB of overhead. Positive finite values are rounded to at least 1 and reduced to fit device limits and a 128 MiB batch budget; other values use the default. Default pixel count / 4, at most 262144. */ photonsPerSample?: number;
96
+ /** Positive first-batch gather radius in scene units, independent of photonsPerSample; shrinks as batches accumulate. Default 3% of the caster bounding sphere radius. */ initialRadius?: number;
97
+ };
98
+ /**
99
+ * WebGPU light tracing for directly visible caustic paths; ignored on WebGL.
100
+ * Default `false`.
101
+ */ lightTracing?: boolean | {
102
+ /** Emitted photons per sample, rounded to a positive integer and capped by the device dispatch limit. Default pixel count / 4. */
103
+ photonsPerSample?: number;
104
+ };
105
+ }
91
106
  /** Optional per-render execution and image-quality settings. */
92
107
  export interface RenderSettings {
93
108
  /** Optional surface outputs. Default []; disabled outputs allocate no AOV resources. */ aovs?: readonly AovKind[];
@@ -95,6 +110,7 @@ export interface RenderSettings {
95
110
  /** Maximum complete samples. Default 2048. */ maxSamples?: number;
96
111
  /**
97
112
  * Maximum scattering depth, rounded and clamped to [1, 20] (NaN becomes 1).
113
+ * Complete caustic light-to-camera paths can be longer than this depth.
98
114
  * Total path events, including transmission and alpha skips, are capped at 20
99
115
  * and at three times this depth. Intel Mac WebGL uses min(depth, 3), doubled
100
116
  * for scenes with transmission or alpha, with its existing early termination.
@@ -102,15 +118,16 @@ export interface RenderSettings {
102
118
  */ bounces?: number;
103
119
  /** Radiance clamp; 0 disables it. Default 0. */ clamping?: number;
104
120
  /** Sampling sequence. Default sobol. */ samplingPattern?: SamplingPattern;
121
+ /** Caustic estimators. All are disabled by default. */ caustics?: CausticSettings;
105
122
  /** Light candidates per vertex; WebGPU only, ignored on WebGL. Default 4. */ directLightCandidates?: number;
106
123
  /** Rounded material edges. Default true. */ bevel?: boolean;
107
124
  /** Bevel neighborhood probes, clamped to 1–32. Default 8. */ bevelSamples?: number;
108
- /** Wavelengths per dispersion sample. Default 3; 1 selects one random wavelength. */ dispersionWavelengths?: 1 | 3;
125
+ /** Wavelength paths per dispersion sample, each carrying one RGB channel. Default 3; 1 selects one random channel. */ dispersionWavelengths?: 1 | 3;
109
126
  /** Primary-ray alpha blend strategy; WebGPU only. Default stochastic. */ alphaBlendStrategy?: AlphaBlendStrategy;
110
127
  /** Minimum transmissive path alpha, using path depth. Default 1. Changes restart this accumulation. */ transmissiveOutputAlphaMin?: number;
111
128
  /** Tile execution budget in milliseconds. Default 21. */ timeBudgetMs?: number;
112
- /** Maximum tiles per frame. Default Infinity. */ maxTiles?: number;
113
- /** Let one refine submit several samples, up to timeBudgetMs of GPU work; WebGPU requires GPU timestamps. RenderLoop also keeps batching between display frames. Default false. */ batchSamples?: boolean;
129
+ /** Maximum tile grid size per sample, rounded up to a rectangular grid. Fewer tiles make each tile larger; this does not cap work per frame or refine call. Default Infinity. */ maxTiles?: number;
130
+ /** Let one refine submit several samples within an estimated timeBudgetMs; one sample or tile can exceed it. WebGPU requires GPU timestamps. RenderLoop also batches between display frames when individual tiles fit the budget. Default false. */ batchSamples?: boolean;
114
131
  /** @deprecated Use batchSamples; this alias will be removed in a later release. Default false. */ allowMultipleSamplesPerFrame?: boolean;
115
132
  /** Adapt scale to frame timing; minScale must not exceed resolutionScale. Default false. */ adaptiveResolution?: {
116
133
  targetFrameMs: number;
@@ -161,6 +178,7 @@ export default class Render {
161
178
  /** Maximum complete samples. Default 2048. */ maxSamples: number;
162
179
  /**
163
180
  * Maximum scattering depth, rounded and clamped to [1, 20] (NaN becomes 1).
181
+ * Complete caustic light-to-camera paths can be longer than this depth.
164
182
  * Total path events, including transmission and alpha skips, are capped at 20
165
183
  * and at three times this depth. Intel Mac WebGL uses min(depth, 3), doubled
166
184
  * for scenes with transmission or alpha, with its existing early termination.
@@ -168,6 +186,7 @@ export default class Render {
168
186
  */ bounces: number;
169
187
  /** Per-sample radiance clamp; 0 disables it. Default 0. */ clamping: number;
170
188
  /** Sampling sequence. Default sobol. */ samplingPattern: SamplingPattern;
189
+ /** Caustic estimators. Default undefined (all disabled). */ caustics?: CausticSettings;
171
190
  /** WebGPU light candidates per vertex; ignored on WebGL. Default 4. */ directLightCandidates: number;
172
191
  /** Rounded material edges. Default true. */ bevel: boolean;
173
192
  /** Bevel probes, clamped to 1–32. Default 8. */ bevelSamples: number;
@@ -175,8 +194,8 @@ export default class Render {
175
194
  /** WebGPU primary-ray alpha policy; ignored on WebGL. Default stochastic. */ alphaBlendStrategy: AlphaBlendStrategy;
176
195
  /** Minimum transmissive path alpha. Default 1. Changing it restarts this render. */ transmissiveOutputAlphaMin: number;
177
196
  /** Execution budget in milliseconds. Default 21. */ timeBudgetMs: number;
178
- /** Maximum tiles per frame. Default Infinity. */ maxTiles: number;
179
- /** Let one refine submit several samples within timeBudgetMs; WebGPU requires GPU timestamps. Default false. */ batchSamples: boolean;
197
+ /** Maximum tile grid size per sample, rounded up to a rectangular grid. Fewer tiles make each tile larger; this does not cap work per frame or refine call. Default Infinity. */ maxTiles: number;
198
+ /** Let one refine submit several samples within an estimated timeBudgetMs; one sample or tile can exceed it. WebGPU requires GPU timestamps. Default false. */ batchSamples: boolean;
180
199
  /** @deprecated Use batchSamples; this alias will be removed in a later release. Default false. */
181
200
  get allowMultipleSamplesPerFrame(): boolean;
182
201
  set allowMultipleSamplesPerFrame(value: boolean);
@@ -210,6 +229,7 @@ export default class Render {
210
229
  private _disposed;
211
230
  private _busy;
212
231
  private _refineOptions?;
232
+ private _refillController?;
213
233
  private _offlineCamera;
214
234
  private _input;
215
235
  private _adaptiveScale;
@@ -238,22 +258,33 @@ export default class Render {
238
258
  private _lastDenoiseTime;
239
259
  private _warnedDenoisedReadback;
240
260
  constructor(engine: Engine, options?: RenderOptions);
241
- /** Actual target width; before allocation uses the requested dimensions. */ get width(): number;
242
- /** Actual target height; before allocation uses the requested dimensions. */ get height(): number;
261
+ /** Output width: exact for fixed renders; otherwise the target width, or the requested width before allocation. */ get width(): number;
262
+ /** Output height: exact for fixed renders; otherwise the target height, or the requested height before allocation. */ get height(): number;
243
263
  /** Complete beauty samples since the last restart; stale engine inputs report 0. Default 0. */
244
264
  get samples(): number;
245
265
  /** Whether complete beauty samples reached maxSamples. Default false. */ get converged(): boolean;
246
266
  /** Current execution state. Default preparing. */ get status(): RenderStatus;
247
267
  private get _current();
248
- /** Latest WebGPU full beauty sample estimate in milliseconds. Default undefined; also undefined for stale inputs, during converge or without GPU timestamps. */
268
+ /** @deprecated Use refineUntil for custom frame loops; this will be removed in a later release. Latest WebGPU full beauty sample estimate in milliseconds, extrapolated from tile GPU time when tiled. Default undefined; also undefined for stale inputs, during converge or without GPU timestamps. */
249
269
  get gpuSampleTimeMs(): number | undefined;
250
- /** Waits for the next release of WebGPU sampling queue capacity. Resolves immediately if no sampling is queued or on WebGL; does not submit work. Throws after disposal. */
251
- whenReadyToRefine(): Promise<void>;
270
+ /**
271
+ * @deprecated Use refineUntil; this will be removed in a later release. Waits for the
272
+ * next release of WebGPU sampling queue capacity and returns true when sampling was
273
+ * queued. Returns false immediately with no queued sampling, on WebGL, or for only
274
+ * denoising, display or offline convergence work. Does not submit work. Throws after
275
+ * disposal.
276
+ */
277
+ whenReadyToRefine(): Promise<boolean>;
252
278
  /** Complete surface-output samples since restart; stale inputs report 0. Independent of beauty samples. Default 0. */
253
279
  get aovSamplesRendered(): number;
254
280
  private _refineAov;
255
281
  private _validate;
256
282
  private _size;
283
+ /**
284
+ * Backends may round targets up. A fixed render widens the view over the extra
285
+ * pixels so the requested centered rectangle keeps its framing and square pixels.
286
+ */
287
+ private _targetCamera;
257
288
  private _settings;
258
289
  private _synchronize;
259
290
  private _trace;
@@ -261,6 +292,23 @@ export default class Render {
261
292
  private _cancelAdaptiveFrames;
262
293
  /** Accumulates within this frame's budget. Default undefined traces beauty and requested AOVs. Pass an enabled AOV kind to refine surface outputs only, without beauty or denoising; progress is aovSamplesRendered. Options default to this render's execution settings. Rejects calls during converge or after disposal. */
263
294
  refine(channel?: AovKind, options?: RefineOptions): RenderStatus;
295
+ /**
296
+ * Calls refine() once synchronously, then keeps sampling on WebGPU until deadline.
297
+ * Call present() right after it; later samples appear at the next presentation.
298
+ * Sampling continues only with batchSamples on, the final channel displayed, a
299
+ * visible page and tiles that fit timeBudgetMs. It stops on abort or convergence.
300
+ * Preparing, compiling, denoising and the first incomplete sample advance only
301
+ * through the synchronous call, which is all WebGL runs. A new call ends the
302
+ * previous one. Asynchronous errors set status to error and resolve; synchronous
303
+ * errors follow refine().
304
+ * @param deadline End time in performance.now() milliseconds.
305
+ * @param options.signal Optional application cancellation; default none. An
306
+ * already aborted signal still permits the synchronous refine().
307
+ */
308
+ refineUntil(deadline: number, options?: {
309
+ signal?: AbortSignal;
310
+ }): Promise<void>;
311
+ private _refineToDeadline;
264
312
  private _refine;
265
313
  private _refineOne;
266
314
  private _schedule;
package/lib/Render.js CHANGED
@@ -104,6 +104,7 @@ export default class Render {
104
104
  /** Maximum complete samples. Default 2048. */ maxSamples = 2048;
105
105
  /**
106
106
  * Maximum scattering depth, rounded and clamped to [1, 20] (NaN becomes 1).
107
+ * Complete caustic light-to-camera paths can be longer than this depth.
107
108
  * Total path events, including transmission and alpha skips, are capped at 20
108
109
  * and at three times this depth. Intel Mac WebGL uses min(depth, 3), doubled
109
110
  * for scenes with transmission or alpha, with its existing early termination.
@@ -111,6 +112,7 @@ export default class Render {
111
112
  */ bounces = 3;
112
113
  /** Per-sample radiance clamp; 0 disables it. Default 0. */ clamping = 0;
113
114
  /** Sampling sequence. Default sobol. */ samplingPattern = 'sobol';
115
+ /** Caustic estimators. Default undefined (all disabled). */ caustics;
114
116
  /** WebGPU light candidates per vertex; ignored on WebGL. Default 4. */ directLightCandidates = 4;
115
117
  /** Rounded material edges. Default true. */ bevel = true;
116
118
  /** Bevel probes, clamped to 1–32. Default 8. */ bevelSamples = 8;
@@ -118,8 +120,8 @@ export default class Render {
118
120
  /** WebGPU primary-ray alpha policy; ignored on WebGL. Default stochastic. */ alphaBlendStrategy = 'stochastic';
119
121
  /** Minimum transmissive path alpha. Default 1. Changing it restarts this render. */ transmissiveOutputAlphaMin = 1;
120
122
  /** Execution budget in milliseconds. Default 21. */ timeBudgetMs = 21;
121
- /** Maximum tiles per frame. Default Infinity. */ maxTiles = Infinity;
122
- /** Let one refine submit several samples within timeBudgetMs; WebGPU requires GPU timestamps. Default false. */ batchSamples = false;
123
+ /** Maximum tile grid size per sample, rounded up to a rectangular grid. Fewer tiles make each tile larger; this does not cap work per frame or refine call. Default Infinity. */ maxTiles = Infinity;
124
+ /** Let one refine submit several samples within an estimated timeBudgetMs; one sample or tile can exceed it. WebGPU requires GPU timestamps. Default false. */ batchSamples = false;
123
125
  /** @deprecated Use batchSamples; this alias will be removed in a later release. Default false. */
124
126
  get allowMultipleSamplesPerFrame() { return this.batchSamples; }
125
127
  set allowMultipleSamplesPerFrame(value) { this.batchSamples = value; }
@@ -180,6 +182,7 @@ export default class Render {
180
182
  _disposed = false;
181
183
  _busy = false;
182
184
  _refineOptions;
185
+ _refillController;
183
186
  _offlineCamera = false;
184
187
  _input = { scene: -1, camera: -1, settings: '' };
185
188
  _adaptiveScale = 1;
@@ -231,8 +234,8 @@ export default class Render {
231
234
  if (denoiser)
232
235
  void this.setDenoiser(denoiser).catch(error => this._engine._notifyError(error));
233
236
  }
234
- /** Actual target width; before allocation uses the requested dimensions. */ get width() { return this._target?.getMemory().width || this._size().width; }
235
- /** Actual target height; before allocation uses the requested dimensions. */ get height() { return this._target?.getMemory().height || this._size().height; }
237
+ /** Output width: exact for fixed renders; otherwise the target width, or the requested width before allocation. */ get width() { return this._fixed?.width ?? (this._target?.getMemory().width || this._size().width); }
238
+ /** Output height: exact for fixed renders; otherwise the target height, or the requested height before allocation. */ get height() { return this._fixed?.height ?? (this._target?.getMemory().height || this._size().height); }
236
239
  /** Complete beauty samples since the last restart; stale engine inputs report 0. Default 0. */
237
240
  get samples() { return this._current ? this._target?.getTotalSamplesRendered() ?? 0 : 0; }
238
241
  /** Whether complete beauty samples reached maxSamples. Default false. */ get converged() { return this.samples >= this.maxSamples; }
@@ -246,13 +249,29 @@ export default class Render {
246
249
  /** @internal */ get _presentable() { return this._complete && (!this._denoisePending || this._denoisedSamples > 0 || (this._schedule() === 'converged' && this.maxSamples > 1)); }
247
250
  /** @internal */ get _output() { if (this._disposed)
248
251
  throw new Error('Render is disposed.'); return this._target; }
249
- /** Latest WebGPU full beauty sample estimate in milliseconds. Default undefined; also undefined for stale inputs, during converge or without GPU timestamps. */
250
- get gpuSampleTimeMs() { return !this._busy && this._current ? this._target?.getRealtimeSampleTime?.() : undefined; }
251
- /** Waits for the next release of WebGPU sampling queue capacity. Resolves immediately if no sampling is queued or on WebGL; does not submit work. Throws after disposal. */
252
- whenReadyToRefine() {
252
+ /** @deprecated Use refineUntil for custom frame loops; this will be removed in a later release. Latest WebGPU full beauty sample estimate in milliseconds, extrapolated from tile GPU time when tiled. Default undefined; also undefined for stale inputs, during converge or without GPU timestamps. */
253
+ get gpuSampleTimeMs() { return this._gpuSampleTimeMs; }
254
+ /** @internal Latest WebGPU full beauty sample estimate in milliseconds; with tiling, extrapolated from tile GPU time. Default undefined; also undefined for stale inputs, during converge or without GPU timestamps. */
255
+ get _gpuSampleTimeMs() { return !this._busy && this._current ? this._target?.getRealtimeSampleTime?.() : undefined; }
256
+ /** @internal Latest WebGPU beauty tile estimate in milliseconds, including amortized caustic work; equal to the sample estimate without tiling. Default undefined; also undefined for stale inputs, during converge or without GPU timestamps. */
257
+ get gpuTileTimeMs() { return !this._busy && this._current ? this._target?.getRealtimeTileTime?.() : undefined; }
258
+ /**
259
+ * @deprecated Use refineUntil; this will be removed in a later release. Waits for the
260
+ * next release of WebGPU sampling queue capacity and returns true when sampling was
261
+ * queued. Returns false immediately with no queued sampling, on WebGL, or for only
262
+ * denoising, display or offline convergence work. Does not submit work. Throws after
263
+ * disposal.
264
+ */
265
+ whenReadyToRefine() { return this._whenReadyToRefine(); }
266
+ /** @internal Waits for the next release of WebGPU sampling queue capacity and returns true
267
+ * when sampling was queued. Returns false immediately with no queued sampling,
268
+ * on WebGL, or for only denoising, display or offline convergence work.
269
+ * Does not submit work. Throws after disposal.
270
+ */
271
+ _whenReadyToRefine() {
253
272
  if (this._disposed)
254
273
  throw new Error('Render is disposed.');
255
- return this._target?.whenTraceAvailable?.() ?? Promise.resolve();
274
+ return !this._busy ? this._target?.whenTraceAvailable?.() ?? Promise.resolve(false) : Promise.resolve(false);
256
275
  }
257
276
  /** Complete surface-output samples since restart; stale inputs report 0. Independent of beauty samples. Default 0. */
258
277
  get aovSamplesRendered() { return this._current ? this._target?.getAovSamples?.() ?? 0 : 0; }
@@ -289,7 +308,7 @@ export default class Render {
289
308
  return this._status = 'preparing';
290
309
  const before = target.getAovSamples?.() ?? 0;
291
310
  const limit = kind === 'id' || kind === 'motion' ? 1 : Math.min(this.maxSamples, this.aovSamples);
292
- if (!target.refineAovs?.(this._engine._inputs.value, Math.min(limit, (target.getAovSamples?.() ?? 0) + 1)))
311
+ if (!target.refineAovs?.(this._targetCamera(this._engine._inputs.value), Math.min(limit, (target.getAovSamples?.() ?? 0) + 1)))
293
312
  return this._status = 'compiling';
294
313
  if ((target.getAovSamples?.() ?? 0) > before) {
295
314
  this._frameTime = Math.max(.001, performance.now() - started);
@@ -317,6 +336,45 @@ export default class Render {
317
336
  const scale = this.adaptiveResolution ? Math.min(this._adaptiveScale, this._resolutionScale) : this._resolutionScale;
318
337
  return { width: Math.max(1, Math.round(this._engine.display.canvas.width * scale)), height: Math.max(1, Math.round(this._engine.display.canvas.height * scale)) };
319
338
  }
339
+ /**
340
+ * Backends may round targets up. A fixed render widens the view over the extra
341
+ * pixels so the requested centered rectangle keeps its framing and square pixels.
342
+ */
343
+ _targetCamera(camera) {
344
+ const memory = this._target?.getMemory(), fixed = this._fixed;
345
+ if (!memory || !fixed || (memory.width === fixed.width && memory.height === fixed.height))
346
+ return camera;
347
+ const scaleX = memory.width / fixed.width, scaleY = memory.height / fixed.height;
348
+ const widened = camera.clone();
349
+ widened.transform = camera.transform;
350
+ if (camera.projection === 'perspective') {
351
+ widened.fov = Math.atan(Math.tan(camera.fov * Math.PI / 360) * scaleY) * 360 / Math.PI;
352
+ widened.aspect = (camera.aspect ?? fixed.width / fixed.height) * scaleX / scaleY;
353
+ // Keeps the lens focal length, and with it depth of field.
354
+ widened.sensorHeight = camera.sensorHeight * scaleY;
355
+ }
356
+ else {
357
+ const x = (camera.left + camera.right) / 2, y = (camera.bottom + camera.top) / 2;
358
+ widened.left = x + (camera.left - x) * scaleX;
359
+ widened.right = x + (camera.right - x) * scaleX;
360
+ widened.bottom = y + (camera.bottom - y) * scaleY;
361
+ widened.top = y + (camera.top - y) * scaleY;
362
+ }
363
+ return widened;
364
+ }
365
+ /** @internal Crops a target-sized RGBA image to a fixed render's requested rectangle. */
366
+ _cropOutput(image) {
367
+ const fixed = this._fixed;
368
+ if (!fixed || !image.width || (image.width === fixed.width && image.height === fixed.height))
369
+ return image;
370
+ const { width, height } = fixed, data = new image.data.constructor(width * height * 4);
371
+ const left = (image.width - width) >> 1, top = (image.height - height) >> 1;
372
+ for (let y = 0; y < height; y++) {
373
+ const start = ((y + top) * image.width + left) * 4;
374
+ data.set(image.data.subarray(start, start + width * 4), y * width * 4);
375
+ }
376
+ return { width, height, data };
377
+ }
320
378
  _settings(sampleCap = this.maxSamples) {
321
379
  const settings = collectTraceSettings(this, sampleCap);
322
380
  settings.batchSamples = this._refineOptions?.batchSamples ?? settings.batchSamples;
@@ -365,7 +423,7 @@ export default class Render {
365
423
  this._sampleStart = performance.now();
366
424
  this._sampleScale = this._adaptiveScale;
367
425
  }
368
- const compiling = target.trace(this._engine._inputs.value, limit);
426
+ const compiling = target.trace(this._targetCamera(this._engine._inputs.value), limit);
369
427
  if (compiling)
370
428
  this._sampleStart = undefined;
371
429
  return compiling;
@@ -427,6 +485,95 @@ export default class Render {
427
485
  this._engine._reportShaderProgress();
428
486
  }
429
487
  }
488
+ /**
489
+ * Calls refine() once synchronously, then keeps sampling on WebGPU until deadline.
490
+ * Call present() right after it; later samples appear at the next presentation.
491
+ * Sampling continues only with batchSamples on, the final channel displayed, a
492
+ * visible page and tiles that fit timeBudgetMs. It stops on abort or convergence.
493
+ * Preparing, compiling, denoising and the first incomplete sample advance only
494
+ * through the synchronous call, which is all WebGL runs. A new call ends the
495
+ * previous one. Asynchronous errors set status to error and resolve; synchronous
496
+ * errors follow refine().
497
+ * @param deadline End time in performance.now() milliseconds.
498
+ * @param options.signal Optional application cancellation; default none. An
499
+ * already aborted signal still permits the synchronous refine().
500
+ */
501
+ refineUntil(deadline, options) {
502
+ return this._refineToDeadline(deadline, options?.signal);
503
+ }
504
+ _refineToDeadline(deadline, signal, options, beforeRefill) {
505
+ this._refillController?.abort();
506
+ const controller = this._refillController = new AbortController();
507
+ // Keep lifecycle exceptions synchronous, just like refine(). Install the
508
+ // controller first so a synchronous sample listener can revoke this call.
509
+ try {
510
+ this.refine(undefined, options);
511
+ }
512
+ catch (error) {
513
+ controller.abort();
514
+ if (this._refillController === controller)
515
+ this._refillController = undefined;
516
+ throw error;
517
+ }
518
+ const device = this._engine.device;
519
+ const canContinue = (afterWait = false) => !controller.signal.aborted && !signal?.aborted &&
520
+ !this._disposed && !this._busy && device !== undefined && device === this._engine.device &&
521
+ (typeof document === 'undefined' || !document.hidden) && performance.now() < deadline &&
522
+ this.batchSamples && this.status === 'converging' && !this.denoising && this.samples > 0 &&
523
+ this.gpuTileTimeMs !== undefined && this.gpuTileTimeMs <= this.timeBudgetMs &&
524
+ this._engine.display.channel === 'final' && (!afterWait || this._engine._isPresented(this._target));
525
+ if (!canContinue()) {
526
+ if (this._refillController === controller)
527
+ this._refillController = undefined;
528
+ return Promise.resolve();
529
+ }
530
+ return (async () => {
531
+ let timer;
532
+ const abort = () => controller.abort();
533
+ const visibility = () => { if (document.hidden)
534
+ abort(); };
535
+ let stopped;
536
+ const cancelled = new Promise(resolve => { stopped = () => resolve(false); });
537
+ controller.signal.addEventListener('abort', stopped, { once: true });
538
+ signal?.addEventListener('abort', abort, { once: true });
539
+ if (typeof document !== 'undefined')
540
+ document.addEventListener('visibilitychange', visibility);
541
+ if (Number.isFinite(deadline))
542
+ timer = setTimeout(abort, Math.min(2147483647, Math.max(0, deadline - performance.now())));
543
+ try {
544
+ while (canContinue()) {
545
+ if (!await Promise.race([this._whenReadyToRefine(), cancelled]))
546
+ break;
547
+ beforeRefill?.();
548
+ if (!canContinue(true))
549
+ break;
550
+ this.refine(undefined, { targetBatchTimeMs: 4, deferResolve: true });
551
+ }
552
+ }
553
+ catch (error) {
554
+ if (!controller.signal.aborted && !this._disposed) {
555
+ this._engine._setRenderError(this._engine._notifyError(error));
556
+ this._status = 'error';
557
+ }
558
+ }
559
+ finally {
560
+ if (timer !== undefined)
561
+ clearTimeout(timer);
562
+ signal?.removeEventListener('abort', abort);
563
+ controller.signal.removeEventListener('abort', stopped);
564
+ if (typeof document !== 'undefined')
565
+ document.removeEventListener('visibilitychange', visibility);
566
+ if (this._refillController === controller)
567
+ this._refillController = undefined;
568
+ }
569
+ })();
570
+ }
571
+ /** @internal Whether this render currently has a between-frame lease. */
572
+ get _refilling() { return !!this._refillController && !this._refillController.signal.aborted; }
573
+ /** @internal Preserve loop-specific synchronous overrides while using refineUntil. */
574
+ _refineUntil(deadline, signal, options, beforeRefill) {
575
+ return this._refineToDeadline(deadline, signal, options, beforeRefill);
576
+ }
430
577
  _refine() {
431
578
  if (this._engine._initializationError)
432
579
  throw this._engine._initializationError;
@@ -455,7 +602,7 @@ export default class Render {
455
602
  if (this._trace(target, limit))
456
603
  return this._status = 'compiling';
457
604
  }
458
- else if (target.refineAovs && !target.refineAovs(this._engine._inputs.value)) {
605
+ else if (target.refineAovs && !target.refineAovs(this._targetCamera(this._engine._inputs.value))) {
459
606
  return this._status = 'compiling';
460
607
  }
461
608
  this._maybeDenoise(false, limit);
@@ -572,9 +719,10 @@ export default class Render {
572
719
  });
573
720
  }
574
721
  _cancelDenoise() { this._denoiseController?.abort(); this._denoiseController = undefined; this._denoiseRevision++; this._target?.cancelDenoise?.(); this._denoisePending = undefined; this._denoisedSamples = 0; }
575
- /** @internal */ _inputsChanged(resetTiming = false) { this._cancelDenoise(); if (resetTiming)
722
+ /** @internal */ _inputsChanged(resetTiming = false) { this._refillController?.abort(); this._cancelDenoise(); if (resetTiming)
576
723
  this._target?.invalidateTiming?.(); }
577
724
  /** @internal */ _deviceLost(recovering) {
725
+ this._refillController?.abort();
578
726
  this._cancelAdaptiveFrames();
579
727
  this._cancelDenoise();
580
728
  this._filterRevision++;
@@ -585,7 +733,7 @@ export default class Render {
585
733
  this._input = { scene: -1, camera: -1, settings: '' };
586
734
  this._status = recovering ? 'preparing' : 'error';
587
735
  }
588
- /** @internal */ _displayChanged() { this._cancelDenoise(); this._target?.invalidateDenoisedOutput(); }
736
+ /** @internal */ _displayChanged() { this._refillController?.abort(); this._cancelDenoise(); this._target?.invalidateDenoisedOutput(); }
589
737
  _maybeDenoise(force = false, sampleTarget = this._nextSampleTarget()) {
590
738
  const target = this._target, filter = this._filter;
591
739
  if (!target || !filter || this._denoisePending || this.samples < 1 || this._denoisedSamples === this.samples)
@@ -624,7 +772,7 @@ export default class Render {
624
772
  if (frameStart !== undefined)
625
773
  this._frameTime = Math.max(this._frameTime, this._lastDenoiseTime - frameStart);
626
774
  }
627
- });
775
+ }, controller.signal);
628
776
  this._denoisePending = task;
629
777
  void task.catch(error => { if (revision === this._denoiseRevision && !this._disposed)
630
778
  this._engine._notifyError(error); }).finally(() => {
@@ -648,7 +796,7 @@ export default class Render {
648
796
  if (!target?.readAov)
649
797
  throw new Error('AOV output is not ready');
650
798
  target.activate();
651
- return target.readAov(aov);
799
+ return this._cropOutput(await target.readAov(aov));
652
800
  }
653
801
  if (!target || this.samples === 0)
654
802
  return { width: 0, height: 0, data: new Float32Array(0) };
@@ -660,7 +808,7 @@ export default class Render {
660
808
  kind = 'hdr';
661
809
  }
662
810
  target.activate();
663
- return target.readOutput(kind);
811
+ return this._cropOutput(await target.readOutput(kind));
664
812
  }
665
813
  /** Borrows a read-only WebGPU texture; undefined on WebGL or before a complete sample. Kind is required. HDR is native resolved linear ray color before denoising/display composition, with DenoiseInput.color normalization and coverage. Display is the latest compose, including filters and display passes. Valid only until this render's next refine/converge, display present/setSize, restart/dispose or device loss; never retain, write or destroy it. */
666
814
  getOutputTexture(kind) {
@@ -670,11 +818,12 @@ export default class Render {
670
818
  }
671
819
  /** Discards only this render's accumulation and pending denoising. */
672
820
  restart() { if (this._disposed)
673
- throw new Error('Render is disposed.'); this._cancelDenoise(); this._cancelAdaptiveFrames(); this._sampleStart = this._frameStart = undefined; this._frameTime = 0; this._target?.restartSamples(); this._status = 'converging'; }
821
+ throw new Error('Render is disposed.'); this._refillController?.abort(); this._cancelDenoise(); this._cancelAdaptiveFrames(); this._sampleStart = this._frameStart = undefined; this._frameTime = 0; this._target?.restartSamples(); this._status = 'converging'; }
674
822
  /** Releases only this render's image resources and denoiser. */
675
823
  dispose() {
676
824
  if (this._disposed)
677
825
  return;
826
+ this._refillController?.abort();
678
827
  this._disposed = true;
679
828
  this._events.clear();
680
829
  this._filterRevision++;
@@ -720,6 +869,7 @@ export default class Render {
720
869
  if (current.scene !== inputs.scene || current.camera !== inputs.camera)
721
870
  throw new Error('Engine inputs changed during converge.');
722
871
  };
872
+ this._refillController?.abort();
723
873
  this._busy = true;
724
874
  const abortConverge = () => this._cancelDenoise();
725
875
  options.signal?.addEventListener('abort', abortConverge, { once: true });
@@ -759,7 +909,7 @@ export default class Render {
759
909
  target.activate();
760
910
  target.setOfflineRenderOptions({ samplesPerBatch: batch, deferPostProcess: true, sampleSequenceOffset: offset });
761
911
  const before = target.getTotalSamplesRendered(), batchStarted = performance.now();
762
- this._status = target.trace(camera, cap) ? 'compiling' : 'converging';
912
+ this._status = target.trace(this._targetCamera(camera), cap) ? 'compiling' : 'converging';
763
913
  const count = target.getTotalSamplesRendered();
764
914
  if (count !== previous) {
765
915
  previous = count;