@meimu/mutracer 0.1.0 → 0.1.1

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.
@@ -14,8 +14,9 @@ loop.start();
14
14
  Each tick updates syncs and commits engine inputs, then calls select, refines the
15
15
  selected render and presents it. Active syncs select preview by default. Isolated
16
16
  edits start final immediately; consecutive changes without explicit sync activity
17
- hold preview for idleMs (default 150 ms). When interaction ends, final starts in
18
- that tick and its ready fallback remains visible until final completes a sample.
17
+ hold preview for idleMs (default 150 ms). When interaction ends, final starts after
18
+ the current preview completes; its fallback remains visible until final completes a sample.
19
+ With intermediate denoising enabled, it stays visible until the first filtered final image is ready.
19
20
 
20
21
  ```ts
21
22
  loop.select = ({changing}) => changing || slider.dragging ? 'preview' : 'final';
@@ -24,20 +25,28 @@ loop.on('sample', ({samples, frameTimeMs}) => updateSamples(samples));
24
25
  ```
25
26
 
26
27
  Loop status is previewing when the displayed target is preview, including fallback
27
- presentation. Preview samples never emit loop sample events or count toward loop
28
+ presentation and the wait for the first complete final image. Starting to trace
29
+ final tiles does not change that status until final is actually presented.
30
+ Preview samples never emit loop sample events or count toward loop
28
31
  samples. Intermediate final filtering reports converging; only terminal filtering
29
32
  reports denoising. After finishing, unchanged ticks update syncs and the engine
30
33
  without GPU submissions. Display or render settings edits resume necessary work.
31
34
 
35
+ During interaction, the loop finishes the current preview sample before applying
36
+ the latest scene/camera edits, keeping previews responsive during continuous input.
37
+
32
38
  start pauses on hidden pages; stop preserves images. Use tick with your own frame
33
39
  loop, or start, which ignores manual tick calls with one warning. loop.converge
34
40
  pauses interactive ticks, converges/presents final and resumes if still started.
35
41
 
36
42
  coalesceSceneUpdates defaults to false. When enabled, preview inference defers
37
- engine commits while syncs keep collecting edits. Completed inference is presented
43
+ engine commits while syncs keep collecting edits. The last presented image stays
44
+ visible until the next filtered preview is ready. Completed inference is presented
38
45
  before the latest camera/scene is committed together. Interaction ending during
39
46
  inference waits for that result before final starts. With it disabled, changes
40
- immediately invalidate old inference. Render.denoising includes intermediate
47
+ immediately invalidate old inference. Once final is available, unused fallback
48
+ inference does not delay it or switch presentation back to preview.
49
+ Render.denoising includes intermediate
41
50
  inference without changing status semantics. Denoising does not enable preview;
42
51
  preview: false always disables it.
43
52
 
@@ -54,7 +63,8 @@ engine.scene = scene;
54
63
  engine.camera = camera;
55
64
  const preview = new Render(engine, {
56
65
  resolutionScale: .5,
57
- adaptiveResolution: { targetFrameMs: 20, minScale: .25 },
66
+ timeBudgetMs: 80,
67
+ adaptiveResolution: { targetFrameMs: 80, minScale: .125 },
58
68
  bounces: 2, bevel: false, dispersionWavelengths: 1,
59
69
  directLightCandidates: 2,
60
70
  });
@@ -106,9 +116,11 @@ usable.
106
116
  fallback and denoiser. `transmissiveOutputAlphaMin` belongs to Render because
107
117
  it is evaluated along individual paths, so changing it restarts that render.
108
118
 
109
- RenderLoop creates a half-resolution preview with adaptive scale .25–.5,
110
- 20 ms target, 2 bounces, no bevel, one wavelength and two light candidates;
119
+ RenderLoop creates a half-resolution preview with adaptive scale .125–.5,
120
+ 80 ms budget, 2 bounces, no bevel, one wavelength and two light candidates;
111
121
  other settings use Render defaults. Pass preview settings, a Render or false.
122
+ Preview resolution drops when interaction is slow and recovers gradually when
123
+ performance improves. Final resolution stays independent of preview adaptation.
112
124
  Pass final settings or a Render; newly created finals use preview as fallback.
113
125
  Supplied Render/Sync objects must belong to this engine. loop.dispose releases
114
126
  only renders it created; dispose supplied renders, syncs and controls yourself.
@@ -118,6 +130,10 @@ provide both fixed width and height at construction; fixed dimensions cannot be
118
130
  changed through `resolutionScale`. `width` and `height` report the actual GPU
119
131
  image, including WebGPU's 16-pixel alignment. See [API options](api.md).
120
132
 
133
+ Keep the display canvas at the viewport's drawing-buffer size and set
134
+ `resolutionScale` on each render independently. Lowering final's scale does not
135
+ lower preview's scale; resizing the display canvas affects both.
136
+
121
137
  Call `render.setDenoiser({ type: 'jnlm', schedule: 'converged' })` with JNLM or an OIDN
122
138
  configuration. The schedules are `converged` (default), `progressive` (first
123
139
  sample, then 4, 8, 16, … with a ten-second limit), and `every-sample`. Same-engine
package/docs/threejs.md CHANGED
@@ -193,8 +193,9 @@ Construction subscribes to control events. The first update converts and writes
193
193
  sources to the engine. `scene` exposes the retained converted scene, or undefined
194
194
  when no source was supplied. A camera-only sync preserves a MuTracer-built scene.
195
195
  `active` covers start/end interaction, camera movement, damping and changing
196
- skinned/morph poses. The damping tail ends when movement and zoom fall below
197
- 0.05% of view distance, allowing final refinement in that same tick.
196
+ skinned/morph poses. OrbitControls damping stays consistent across render frame
197
+ rates, and final refinement resumes when motion settles. Use
198
+ `updateControls: false` when your application already updates the controls.
198
199
 
199
200
  `fromTHREEScene(scene, options?)` and `fromTHREECamera(camera, options?)` remain
200
201
  available for one-shot conversions. Each call reruns conversion hooks and creates fresh default objects; custom
package/lib/Display.d.ts CHANGED
@@ -65,7 +65,7 @@ export default class Display {
65
65
  addPostProcess(pass: PostProcess): () => void;
66
66
  private warnPostProcess;
67
67
  private compose;
68
- /** Presents a complete render or its fallback. Does not trace; leaves canvas unchanged if neither is ready. */
68
+ /** Presents a complete image or fallback. Keeps fallback visible until the first intermediate filter finishes. Does not trace. */
69
69
  present(render: Render): Render | undefined;
70
70
  /** Encodes this render at its actual dimensions using these settings; leaves the display canvas unchanged. */
71
71
  toBlob(render: Render, options?: BlobOptions): Promise<Blob>;
package/lib/Display.js CHANGED
@@ -86,11 +86,13 @@ export default class Display {
86
86
  }
87
87
  return target;
88
88
  }
89
- /** Presents a complete render or its fallback. Does not trace; leaves canvas unchanged if neither is ready. */
89
+ /** Presents a complete image or fallback. Keeps fallback visible until the first intermediate filter finishes. Does not trace. */
90
90
  present(render) {
91
91
  if (render._engine !== this.engine)
92
92
  throw new Error('Render belongs to another engine.');
93
- const selected = render._complete ? render : render.fallback?._complete ? render.fallback : undefined;
93
+ const ready = this.channel === 'final' && render.fallback ? render._presentable : render._complete;
94
+ const fallback = render.fallback;
95
+ const selected = ready ? render : fallback && (this.channel === 'final' ? fallback._presentable : fallback._complete) ? fallback : undefined;
94
96
  if (!selected)
95
97
  return;
96
98
  const target = this.compose(selected);
@@ -1 +1 @@
1
- {"version":3,"file":"Display.js","sourceRoot":"","sources":["../src/Display.ts"],"names":[],"mappings":"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","sourcesContent":["import {createCanvas} from './util/canvas.js';\nimport type Engine from './Engine.js';\nimport type Render from './Render.js';\nimport type { ToneMapping, DisplayChannel } from './renderer/RenderingPipeline.js';\nimport type { ColorRGB } from './types.js';\nimport { displaySettingsKey, applyDisplaySettings } from './settings/display.js';\n/** Display image channels. Default final. */\nexport type {DisplayChannel} from './renderer/RenderingPipeline.js';\n/** Image encoding options for Display.toBlob. */\nexport interface BlobOptions {\n /** Browser image MIME type. Default image/png. */ type?: string;\n /** Encoder quality in 0–1. Default undefined uses the browser default. */ quality?: number;\n}\n/** Optional shared display settings; defaults match Display. */\nexport interface DisplaySettings {\n /** Tone mapping transform. Default agx. */ toneMapping?: ToneMapping;\n /** Linear exposure multiplier. Default 1. */ toneMappingExposure?: number;\n /** Contrast multiplier. Default 1. */ contrast?: number;\n /** Saturation multiplier. Default 1. */ saturation?: number;\n /** White balance temperature shift in -1–1. Default 0. */ temperature?: number;\n /** Green/magenta tint in -1–1. Default 0. */ tint?: number;\n /** Vignette strength in 0–1. Default 0. */ vignette?: number;\n /** Bloom intensity; 0 disables it. Default 0. */ bloomIntensity?: number;\n /** Bloom luminance threshold. Default 1. */ bloomThreshold?: number;\n /** Bloom radius in 0–1. Default 0.6. */ bloomRadius?: number;\n /** Extended-range WebGPU presentation, ignored on WebGL. Default false. */ hdrOutputEnabled?: boolean;\n /** Display image channel. Default final. */ channel?: DisplayChannel;\n /** Linear shadow-catcher color. Default black. */ shadowCatcherColor?: ColorRGB;\n /** Shadow-catcher opacity multiplier. Default 1. */ shadowCatcherAlpha?: number;\n}\n/** Borrowed WebGPU images for one synchronous display pass. */\nexport interface PostProcessContext {\n /** Engine device; may change after recovery. */ device: GPUDevice;\n /** Record commands here; the library finishes and submits it. */ encoder: GPUCommandEncoder;\n /** Read-only composed display image, top row first; no additional color conversion. */ input: GPUTexture;\n /** Fully write this image. Same format and dimensions as input; supports render, storage and copy writes. */ output: GPUTexture;\n /** Actual render width. */ width: number;\n /** Actual render height. */ height: number;\n /** Selected complete render, including a selected fallback. */ render: Render;\n}\n/** Synchronous display pass. Images and encoder are borrowed only during the call; do not retain or destroy them. */\nexport type PostProcess = (context: PostProcessContext) => void;\n/** Canvas presentation and shared display settings; obtained from engine.display. */\nexport default class Display {\n /** Display canvas. Default the supplied Engine canvas, or a new canvas (OffscreenCanvas without document). */ readonly canvas: HTMLCanvasElement | OffscreenCanvas;\n /** Tone mapping transform. Default agx. */ toneMapping: ToneMapping = 'agx';\n /** Linear exposure multiplier. Default 1. */ toneMappingExposure = 1;\n /** Contrast multiplier. Default 1. */ contrast = 1;\n /** Saturation multiplier. Default 1. */ saturation = 1;\n /** White balance temperature shift in -1–1. Default 0. */ temperature = 0;\n /** Green/magenta tint in -1–1. Default 0. */ tint = 0;\n /** Vignette strength in 0–1. Default 0. */ vignette = 0;\n /** Bloom intensity; 0 disables it. Default 0. */ bloomIntensity = 0;\n /** Bloom luminance threshold. Default 1. */ bloomThreshold = 1;\n /** Bloom radius in 0–1. Default 0.6. */ bloomRadius = 0.6;\n /** Extended-range WebGPU presentation, ignored on WebGL. Default false. */ hdrOutputEnabled = false;\n /** Display image channel. Default final. */ channel: DisplayChannel = 'final';\n /** Linear shadow-catcher color. Default black. */ shadowCatcherColor: ColorRGB = [0, 0, 0];\n /** Shadow-catcher opacity multiplier. Default 1. */ shadowCatcherAlpha = 1;\n private constructor(private readonly engine: Engine, canvas: HTMLCanvasElement | OffscreenCanvas) { this.canvas = canvas; }\n /** @internal */ static _create(engine: Engine, canvas: HTMLCanvasElement | OffscreenCanvas) { return new Display(engine, canvas); }\n /** Resizes the drawing buffer; scale-based renders resize at their next refine. */\n setSize(width: number, height: number) {\n if (!Number.isFinite(width) || !Number.isFinite(height) || width < 1 || height < 1) throw new Error('Display dimensions must be positive.');\n width = Math.round(width); height = Math.round(height);\n if (this.canvas.width !== width) this.canvas.width = width;\n if (this.canvas.height !== height) this.canvas.height = height;\n }\n private readonly settings = new WeakMap<Render, string>();\n /** @internal */ _configure(render: Render) {\n if (render._engine !== this.engine) throw new Error('Render belongs to another engine.');\n const target = render._output;\n if (!target) return;\n const key = displaySettingsKey(this);\n const previous = this.settings.get(render);\n if (previous !== key) {\n this.settings.set(render, key);\n if (previous !== undefined) render._displayChanged();\n }\n target.activate();\n applyDisplaySettings(this, target);\n }\n private readonly _postProcesses: {pass: PostProcess}[] = [];\n private _warnedPostProcess = false;\n /** Registers a display pass in order and returns an idempotent removal function. Default no passes. WebGPU only; WebGL skips passes with one warning. Runs after composition on every present/toBlob, without tracing or invalidating denoising. A thrown error discards the whole chain, presents the original image and emits engine error. */\n addPostProcess(pass: PostProcess): () => void {\n const entry = {pass};\n this._postProcesses.push(entry);\n if (this.engine.backend === 'webgl') this.warnPostProcess();\n return () => { const index = this._postProcesses.indexOf(entry); if (index >= 0) this._postProcesses.splice(index, 1); };\n }\n private warnPostProcess() {\n if (!this._warnedPostProcess) { this._warnedPostProcess = true; console.warn('Display post-processing requires WebGPU; passes are ignored on WebGL.'); }\n }\n private compose(render: Render) {\n if (render._engine !== this.engine) throw new Error('Render belongs to another engine.');\n const target = render._output;\n if (!target?.presentationComplete) return;\n this._configure(render);\n target.drawPostProcess();\n if (this._postProcesses.length && this.engine.backend === 'webgl') this.warnPostProcess();\n try { target.applyPostProcesses?.(this._postProcesses.map(entry => entry.pass), render); }\n catch (error) { this.engine._notifyError(error); }\n return target;\n }\n /** Presents a complete render or its fallback. Does not trace; leaves canvas unchanged if neither is ready. */\n present(render: Render): Render | undefined {\n if (render._engine !== this.engine) throw new Error('Render belongs to another engine.');\n const selected = render._complete ? render : render.fallback?._complete ? render.fallback : undefined;\n if (!selected) return;\n const target = this.compose(selected);\n if (!target) return;\n this.engine._present(target);\n return selected;\n }\n /** Encodes this render at its actual dimensions using these settings; leaves the display canvas unchanged. */\n async toBlob(render: Render, options: BlobOptions = {}): Promise<Blob> {\n const target = this.compose(render);\n if (!target) throw new Error('Render has no complete image.');\n const image = await target.readPresentation();\n const canvas = createCanvas(image.width, image.height, 'convertToBlob' in this.canvas);\n const context = canvas.getContext('2d') as CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D | null;\n if (!context) throw new Error('The image encoding context is not available.');\n const bytes = new Uint8ClampedArray(image.data.length);\n for (let i = 0; i < bytes.length; i++) bytes[i] = Math.round(Math.min(1, Math.max(0, image.data[i])) * 255);\n context.putImageData(new ImageData(bytes, image.width, image.height), 0, 0);\n if ('convertToBlob' in canvas) return canvas.convertToBlob({type: options.type ?? 'image/png', quality: options.quality});\n return new Promise((resolve, reject) => canvas.toBlob(blob => blob ? resolve(blob) : reject(new Error('The image could not be encoded')), options.type ?? 'image/png', options.quality));\n }\n}\n"]}
1
+ {"version":3,"file":"Display.js","sourceRoot":"","sources":["../src/Display.ts"],"names":[],"mappings":"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","sourcesContent":["import {createCanvas} from './util/canvas.js';\nimport type Engine from './Engine.js';\nimport type Render from './Render.js';\nimport type { ToneMapping, DisplayChannel } from './renderer/RenderingPipeline.js';\nimport type { ColorRGB } from './types.js';\nimport { displaySettingsKey, applyDisplaySettings } from './settings/display.js';\n/** Display image channels. Default final. */\nexport type {DisplayChannel} from './renderer/RenderingPipeline.js';\n/** Image encoding options for Display.toBlob. */\nexport interface BlobOptions {\n /** Browser image MIME type. Default image/png. */ type?: string;\n /** Encoder quality in 0–1. Default undefined uses the browser default. */ quality?: number;\n}\n/** Optional shared display settings; defaults match Display. */\nexport interface DisplaySettings {\n /** Tone mapping transform. Default agx. */ toneMapping?: ToneMapping;\n /** Linear exposure multiplier. Default 1. */ toneMappingExposure?: number;\n /** Contrast multiplier. Default 1. */ contrast?: number;\n /** Saturation multiplier. Default 1. */ saturation?: number;\n /** White balance temperature shift in -1–1. Default 0. */ temperature?: number;\n /** Green/magenta tint in -1–1. Default 0. */ tint?: number;\n /** Vignette strength in 0–1. Default 0. */ vignette?: number;\n /** Bloom intensity; 0 disables it. Default 0. */ bloomIntensity?: number;\n /** Bloom luminance threshold. Default 1. */ bloomThreshold?: number;\n /** Bloom radius in 0–1. Default 0.6. */ bloomRadius?: number;\n /** Extended-range WebGPU presentation, ignored on WebGL. Default false. */ hdrOutputEnabled?: boolean;\n /** Display image channel. Default final. */ channel?: DisplayChannel;\n /** Linear shadow-catcher color. Default black. */ shadowCatcherColor?: ColorRGB;\n /** Shadow-catcher opacity multiplier. Default 1. */ shadowCatcherAlpha?: number;\n}\n/** Borrowed WebGPU images for one synchronous display pass. */\nexport interface PostProcessContext {\n /** Engine device; may change after recovery. */ device: GPUDevice;\n /** Record commands here; the library finishes and submits it. */ encoder: GPUCommandEncoder;\n /** Read-only composed display image, top row first; no additional color conversion. */ input: GPUTexture;\n /** Fully write this image. Same format and dimensions as input; supports render, storage and copy writes. */ output: GPUTexture;\n /** Actual render width. */ width: number;\n /** Actual render height. */ height: number;\n /** Selected complete render, including a selected fallback. */ render: Render;\n}\n/** Synchronous display pass. Images and encoder are borrowed only during the call; do not retain or destroy them. */\nexport type PostProcess = (context: PostProcessContext) => void;\n/** Canvas presentation and shared display settings; obtained from engine.display. */\nexport default class Display {\n /** Display canvas. Default the supplied Engine canvas, or a new canvas (OffscreenCanvas without document). */ readonly canvas: HTMLCanvasElement | OffscreenCanvas;\n /** Tone mapping transform. Default agx. */ toneMapping: ToneMapping = 'agx';\n /** Linear exposure multiplier. Default 1. */ toneMappingExposure = 1;\n /** Contrast multiplier. Default 1. */ contrast = 1;\n /** Saturation multiplier. Default 1. */ saturation = 1;\n /** White balance temperature shift in -1–1. Default 0. */ temperature = 0;\n /** Green/magenta tint in -1–1. Default 0. */ tint = 0;\n /** Vignette strength in 0–1. Default 0. */ vignette = 0;\n /** Bloom intensity; 0 disables it. Default 0. */ bloomIntensity = 0;\n /** Bloom luminance threshold. Default 1. */ bloomThreshold = 1;\n /** Bloom radius in 0–1. Default 0.6. */ bloomRadius = 0.6;\n /** Extended-range WebGPU presentation, ignored on WebGL. Default false. */ hdrOutputEnabled = false;\n /** Display image channel. Default final. */ channel: DisplayChannel = 'final';\n /** Linear shadow-catcher color. Default black. */ shadowCatcherColor: ColorRGB = [0, 0, 0];\n /** Shadow-catcher opacity multiplier. Default 1. */ shadowCatcherAlpha = 1;\n private constructor(private readonly engine: Engine, canvas: HTMLCanvasElement | OffscreenCanvas) { this.canvas = canvas; }\n /** @internal */ static _create(engine: Engine, canvas: HTMLCanvasElement | OffscreenCanvas) { return new Display(engine, canvas); }\n /** Resizes the drawing buffer; scale-based renders resize at their next refine. */\n setSize(width: number, height: number) {\n if (!Number.isFinite(width) || !Number.isFinite(height) || width < 1 || height < 1) throw new Error('Display dimensions must be positive.');\n width = Math.round(width); height = Math.round(height);\n if (this.canvas.width !== width) this.canvas.width = width;\n if (this.canvas.height !== height) this.canvas.height = height;\n }\n private readonly settings = new WeakMap<Render, string>();\n /** @internal */ _configure(render: Render) {\n if (render._engine !== this.engine) throw new Error('Render belongs to another engine.');\n const target = render._output;\n if (!target) return;\n const key = displaySettingsKey(this);\n const previous = this.settings.get(render);\n if (previous !== key) {\n this.settings.set(render, key);\n if (previous !== undefined) render._displayChanged();\n }\n target.activate();\n applyDisplaySettings(this, target);\n }\n private readonly _postProcesses: {pass: PostProcess}[] = [];\n private _warnedPostProcess = false;\n /** Registers a display pass in order and returns an idempotent removal function. Default no passes. WebGPU only; WebGL skips passes with one warning. Runs after composition on every present/toBlob, without tracing or invalidating denoising. A thrown error discards the whole chain, presents the original image and emits engine error. */\n addPostProcess(pass: PostProcess): () => void {\n const entry = {pass};\n this._postProcesses.push(entry);\n if (this.engine.backend === 'webgl') this.warnPostProcess();\n return () => { const index = this._postProcesses.indexOf(entry); if (index >= 0) this._postProcesses.splice(index, 1); };\n }\n private warnPostProcess() {\n if (!this._warnedPostProcess) { this._warnedPostProcess = true; console.warn('Display post-processing requires WebGPU; passes are ignored on WebGL.'); }\n }\n private compose(render: Render) {\n if (render._engine !== this.engine) throw new Error('Render belongs to another engine.');\n const target = render._output;\n if (!target?.presentationComplete) return;\n this._configure(render);\n target.drawPostProcess();\n if (this._postProcesses.length && this.engine.backend === 'webgl') this.warnPostProcess();\n try { target.applyPostProcesses?.(this._postProcesses.map(entry => entry.pass), render); }\n catch (error) { this.engine._notifyError(error); }\n return target;\n }\n /** Presents a complete image or fallback. Keeps fallback visible until the first intermediate filter finishes. Does not trace. */\n present(render: Render): Render | undefined {\n if (render._engine !== this.engine) throw new Error('Render belongs to another engine.');\n const ready = this.channel === 'final' && render.fallback ? render._presentable : render._complete;\n const fallback = render.fallback;\n const selected = ready ? render : fallback && (this.channel === 'final' ? fallback._presentable : fallback._complete) ? fallback : undefined;\n if (!selected) return;\n const target = this.compose(selected);\n if (!target) return;\n this.engine._present(target);\n return selected;\n }\n /** Encodes this render at its actual dimensions using these settings; leaves the display canvas unchanged. */\n async toBlob(render: Render, options: BlobOptions = {}): Promise<Blob> {\n const target = this.compose(render);\n if (!target) throw new Error('Render has no complete image.');\n const image = await target.readPresentation();\n const canvas = createCanvas(image.width, image.height, 'convertToBlob' in this.canvas);\n const context = canvas.getContext('2d') as CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D | null;\n if (!context) throw new Error('The image encoding context is not available.');\n const bytes = new Uint8ClampedArray(image.data.length);\n for (let i = 0; i < bytes.length; i++) bytes[i] = Math.round(Math.min(1, Math.max(0, image.data[i])) * 255);\n context.putImageData(new ImageData(bytes, image.width, image.height), 0, 0);\n if ('convertToBlob' in canvas) return canvas.convertToBlob({type: options.type ?? 'image/png', quality: options.quality});\n return new Promise((resolve, reject) => canvas.toBlob(blob => blob ? resolve(blob) : reject(new Error('The image could not be encoded')), options.type ?? 'image/png', options.quality));\n }\n}\n"]}
package/lib/Render.d.ts CHANGED
@@ -197,6 +197,12 @@ export default class Render {
197
197
  private _adaptiveScale;
198
198
  private _lastScale;
199
199
  private _frameTime;
200
+ private _sampleStart?;
201
+ private _frameStart?;
202
+ private _sampleScale;
203
+ private _frameScale;
204
+ private _adaptiveFrames;
205
+ private _adaptiveFastFrames;
200
206
  private _events;
201
207
  /** Owning engine. */ get engine(): Engine;
202
208
  /** Whether inference is queued or running, including intermediate updates. Default false. */ get denoising(): boolean;
@@ -225,6 +231,9 @@ export default class Render {
225
231
  private _size;
226
232
  private _settings;
227
233
  private _synchronize;
234
+ private _trace;
235
+ private _adaptResolution;
236
+ private _cancelAdaptiveFrames;
228
237
  /** Accumulates within this frame's budget. Rejects calls during converge or after disposal. */
229
238
  refine(): RenderStatus;
230
239
  private _refine;
package/lib/Render.js CHANGED
@@ -3,6 +3,7 @@ import { cameraSnapshot } from './renderer/cameraSnapshot.js';
3
3
  import { collectTraceSettings, imageSettingsKey } from './settings/render.js';
4
4
  import { DEFAULT_JNLM_OPTIONS, createJNLMDenoiser } from './renderer/jnlm.js';
5
5
  import { nextPreviewScale } from './renderer/util.js';
6
+ import { scheduleFrame } from './util/frame.js';
6
7
  let oidnModule;
7
8
  const loadOIDN = () => oidnModule ??= import('./renderer/oidnNetwork.js');
8
9
  function validateDenoiserOptions(options) {
@@ -121,6 +122,8 @@ export default class Render {
121
122
  throw new Error('Fixed-size renders cannot use resolutionScale.');
122
123
  if (!Number.isFinite(value) || value <= 0)
123
124
  throw new Error('resolutionScale must be positive.');
125
+ if (value !== this._resolutionScale)
126
+ this._cancelAdaptiveFrames();
124
127
  this._resolutionScale = value;
125
128
  }
126
129
  _fallback = null;
@@ -170,6 +173,12 @@ export default class Render {
170
173
  _adaptiveScale = 1;
171
174
  _lastScale = 1;
172
175
  _frameTime = 0;
176
+ _sampleStart;
177
+ _frameStart;
178
+ _sampleScale = 1;
179
+ _frameScale = 1;
180
+ _adaptiveFrames = new Set();
181
+ _adaptiveFastFrames = 0;
173
182
  _events = new Events();
174
183
  /** Owning engine. */ get engine() { return this._engine; }
175
184
  /** Whether inference is queued or running, including intermediate updates. Default false. */ get denoising() { return !!this._denoisePending; }
@@ -220,6 +229,7 @@ export default class Render {
220
229
  }
221
230
  get _current() { const inputs = this._engine._inputs; return inputs.scene === this._input.scene && inputs.camera === this._input.camera && !this._disposed; }
222
231
  /** @internal */ get _complete() { return this._current && !!this._target?.presentationComplete && !this._target.isFirstFrame() && this.samples > 0; }
232
+ /** @internal */ get _presentable() { return this._complete && (!this._denoisePending || this._denoisedSamples > 0 || (this._schedule() === 'converged' && this.maxSamples > 1)); }
223
233
  /** @internal */ get _output() { if (this._disposed)
224
234
  throw new Error('Render is disposed.'); return this._target; }
225
235
  _validate() {
@@ -259,12 +269,65 @@ export default class Render {
259
269
  this._offlineCamera = false;
260
270
  this._cancelDenoise();
261
271
  target.restartSamples();
272
+ this._sampleStart = this._frameStart = undefined;
273
+ this._frameTime = 0;
262
274
  target.setSize(size.width, size.height);
263
275
  this._input = { scene: inputs.scene, camera: inputs.camera, settings: key };
264
276
  }
265
- target.setOnSampleRendered((samples, isFirstFrame, frameTimeMs) => { this._frameTime = frameTimeMs; this._events.emit('sample', { samples, isFirstFrame, frameTimeMs }); });
277
+ target.setOnSampleRendered((samples, isFirstFrame, frameTimeMs) => {
278
+ // A preview's latency includes every tile, not just its last dispatch.
279
+ this._frameTime = Math.max(frameTimeMs, this._sampleStart === undefined ? 0 : performance.now() - this._sampleStart);
280
+ this._frameStart = this._sampleStart;
281
+ this._frameScale = this._sampleScale;
282
+ this._sampleStart = undefined;
283
+ this._events.emit('sample', { samples, isFirstFrame, frameTimeMs });
284
+ });
266
285
  return target;
267
286
  }
287
+ _trace(target, limit) {
288
+ if (this._sampleStart === undefined) {
289
+ this._sampleStart = performance.now();
290
+ this._sampleScale = this._adaptiveScale;
291
+ }
292
+ const compiling = target.trace(this._engine._inputs.value, limit);
293
+ if (compiling)
294
+ this._sampleStart = undefined;
295
+ return compiling;
296
+ }
297
+ _adaptResolution() {
298
+ if (!this._busy && this.adaptiveResolution && this._frameTime > 0 && this.samples > 0) {
299
+ const { targetFrameMs, minScale } = this.adaptiveResolution;
300
+ const start = this._frameStart, time = this._frameTime, scale = this._frameScale, maxScale = this._resolutionScale;
301
+ const { width, height } = this._engine.display.canvas;
302
+ this._frameTime = 0;
303
+ // Observe the following display frame too: GPU submissions and WebGL
304
+ // filters can return long before their work reaches the screen.
305
+ const cancel = scheduleFrame(() => {
306
+ this._adaptiveFrames.delete(cancel);
307
+ const adaptive = this.adaptiveResolution, canvas = this._engine.display.canvas;
308
+ if (this._disposed || this._busy || !adaptive || adaptive.targetFrameMs !== targetFrameMs || adaptive.minScale !== minScale ||
309
+ this._resolutionScale !== maxScale || canvas.width !== width || canvas.height !== height)
310
+ return;
311
+ const elapsed = Math.max(time, start === undefined ? 0 : performance.now() - start);
312
+ const next = nextPreviewScale(scale, elapsed, targetFrameMs, maxScale, this._adaptiveScale, minScale);
313
+ if (next > this._adaptiveScale) {
314
+ if (++this._adaptiveFastFrames < 4)
315
+ return;
316
+ this._adaptiveScale = Math.min(next, this._adaptiveScale * 1.25);
317
+ }
318
+ else
319
+ this._adaptiveScale = next;
320
+ this._adaptiveFastFrames = 0;
321
+ });
322
+ this._adaptiveFrames.add(cancel);
323
+ }
324
+ }
325
+ _cancelAdaptiveFrames() {
326
+ for (const cancel of this._adaptiveFrames)
327
+ cancel();
328
+ this._adaptiveFrames.clear();
329
+ this._adaptiveFastFrames = 0;
330
+ }
268
331
  /** Accumulates within this frame's budget. Rejects calls during converge or after disposal. */
269
332
  refine() {
270
333
  if (this._busy)
@@ -305,10 +368,10 @@ export default class Render {
305
368
  target.activate();
306
369
  if (this._denoisePending)
307
370
  return this._status = this.converged ? 'denoising' : 'converging';
308
- target.setPreserveDenoisedOutput?.(!!this._filter && this._schedule() === 'progressive');
371
+ target.setPreserveDenoisedOutput?.(!!this._filter && this._schedule() !== 'converged');
309
372
  const limit = this._nextSampleTarget();
310
373
  if (this.samples < limit) {
311
- if (target.trace(this._engine._inputs.value, limit))
374
+ if (this._trace(target, limit))
312
375
  return this._status = 'compiling';
313
376
  }
314
377
  this._maybeDenoise(false, limit);
@@ -316,8 +379,7 @@ export default class Render {
316
379
  return this._status = this.converged ? 'denoising' : 'converging';
317
380
  if (this._filterPending && this.converged)
318
381
  return this._status = 'preparing';
319
- if (this.adaptiveResolution && this._frameTime > 0 && this.samples > 0)
320
- this._adaptiveScale = nextPreviewScale(this._adaptiveScale, this._frameTime, this.adaptiveResolution.targetFrameMs, this._resolutionScale, this.adaptiveResolution.minScale);
382
+ this._adaptResolution();
321
383
  return this._status = this.converged ? 'finished' : 'converging';
322
384
  }
323
385
  _refineOne() {
@@ -329,8 +391,10 @@ export default class Render {
329
391
  return;
330
392
  target.activate();
331
393
  if (this.samples < 1)
332
- this._status = target.trace(this._engine._inputs.value, 1) ? 'compiling' : 'converging';
394
+ this._status = this._trace(target, 1) ? 'compiling' : 'converging';
333
395
  this._maybeDenoise();
396
+ if (!this._denoisePending)
397
+ this._adaptResolution();
334
398
  }
335
399
  _schedule() { return this._denoiserOptions?.schedule ?? 'converged'; }
336
400
  _nextSampleTarget() {
@@ -422,6 +486,7 @@ export default class Render {
422
486
  /** @internal */ _inputsChanged(resetTiming = false) { this._cancelDenoise(); if (resetTiming)
423
487
  this._target?.invalidateTiming?.(); }
424
488
  /** @internal */ _deviceLost(recovering) {
489
+ this._cancelAdaptiveFrames();
425
490
  this._cancelDenoise();
426
491
  this._filterRevision++;
427
492
  this._filter = undefined;
@@ -443,7 +508,7 @@ export default class Render {
443
508
  this._engine.display._configure(this);
444
509
  target.activate();
445
510
  target.drawPostProcess();
446
- const revision = ++this._denoiseRevision, samples = this.samples;
511
+ const revision = ++this._denoiseRevision, samples = this.samples, frameStart = this._frameStart;
447
512
  const controller = this._denoiseController = new AbortController();
448
513
  // One shared scratch lease across all renders; queued stale jobs do no GPU work.
449
514
  const task = this._engine._enqueueDenoise(async () => {
@@ -467,15 +532,20 @@ export default class Render {
467
532
  target.drawPostProcess();
468
533
  this._denoisedSamples = samples;
469
534
  this._lastDenoiseTime = performance.now();
535
+ if (frameStart !== undefined)
536
+ this._frameTime = Math.max(this._frameTime, this._lastDenoiseTime - frameStart);
470
537
  }
471
538
  });
472
539
  this._denoisePending = task;
473
540
  void task.catch(error => { if (revision === this._denoiseRevision && !this._disposed)
474
- this._engine._notifyError(error); }).finally(() => { if (this._denoisePending === task) {
475
- this._denoiseController = undefined;
476
- this._denoisePending = undefined;
477
- this._status = this.converged ? 'finished' : 'converging';
478
- } });
541
+ this._engine._notifyError(error); }).finally(() => {
542
+ if (this._denoisePending === task) {
543
+ this._denoiseController = undefined;
544
+ this._denoisePending = undefined;
545
+ this._status = this.converged ? 'finished' : 'converging';
546
+ this._adaptResolution();
547
+ }
548
+ });
479
549
  }
480
550
  /** Reads native RGBA float32, top row first. Default hdr. Custom denoised output is resolved linear HDR ray color before display background/catcher composition; built-ins retain their native display-space output. Unavailable denoised output falls back to hdr. */
481
551
  async readPixels(kind = 'hdr') {
@@ -502,7 +572,7 @@ export default class Render {
502
572
  }
503
573
  /** Discards only this render's accumulation and pending denoising. */
504
574
  restart() { if (this._disposed)
505
- throw new Error('Render is disposed.'); this._cancelDenoise(); this._target?.restartSamples(); this._status = 'converging'; }
575
+ throw new Error('Render is disposed.'); this._cancelDenoise(); this._cancelAdaptiveFrames(); this._sampleStart = this._frameStart = undefined; this._frameTime = 0; this._target?.restartSamples(); this._status = 'converging'; }
506
576
  /** Releases only this render's image resources and denoiser. */
507
577
  dispose() {
508
578
  if (this._disposed)
@@ -511,6 +581,7 @@ export default class Render {
511
581
  this._events.clear();
512
582
  this._filterRevision++;
513
583
  this._cancelDenoise();
584
+ this._cancelAdaptiveFrames();
514
585
  if (this._target)
515
586
  this._engine._releaseTarget(this._target);
516
587
  if (this._filter && this._filter.kind !== 'custom')