@aardworx/wombat.rendering 0.5.4 → 0.7.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/dist/core/bufferView.d.ts +73 -12
- package/dist/core/bufferView.d.ts.map +1 -1
- package/dist/core/bufferView.js +225 -9
- package/dist/core/bufferView.js.map +1 -1
- package/dist/core/elementType.d.ts +16 -0
- package/dist/core/elementType.d.ts.map +1 -0
- package/dist/core/elementType.js +80 -0
- package/dist/core/elementType.js.map +1 -0
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/renderObject.d.ts +3 -3
- package/dist/core/renderObject.d.ts.map +1 -1
- package/dist/resources/preparedRenderObject.d.ts +2 -2
- package/dist/resources/preparedRenderObject.d.ts.map +1 -1
- package/dist/resources/preparedRenderObject.js +90 -96
- package/dist/resources/preparedRenderObject.js.map +1 -1
- package/dist/window/loop.d.ts +10 -0
- package/dist/window/loop.d.ts.map +1 -1
- package/dist/window/loop.js +63 -15
- package/dist/window/loop.js.map +1 -1
- package/package.json +4 -4
- package/src/core/bufferView.ts +271 -20
- package/src/core/elementType.ts +84 -0
- package/src/core/index.ts +5 -1
- package/src/core/renderObject.ts +3 -3
- package/src/resources/preparedRenderObject.ts +126 -90
- package/src/window/loop.ts +78 -15
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
import {
|
|
16
16
|
AdaptiveResource,
|
|
17
|
+
BufferView,
|
|
18
|
+
ElementType,
|
|
17
19
|
type CompiledEffect,
|
|
18
20
|
type FramebufferSignature,
|
|
19
21
|
type ProgramInterface,
|
|
@@ -25,7 +27,6 @@ import {
|
|
|
25
27
|
HashMap,
|
|
26
28
|
cval,
|
|
27
29
|
} from "@aardworx/wombat.adaptive";
|
|
28
|
-
import type { BufferView } from "../core/bufferView.js";
|
|
29
30
|
import type { Type } from "@aardworx/wombat.shader/ir";
|
|
30
31
|
import { prepareAdaptiveBuffer } from "./adaptiveBuffer.js";
|
|
31
32
|
import { prepareAdaptiveTexture } from "./adaptiveTexture.js";
|
|
@@ -34,32 +35,6 @@ import { prepareUniformBuffer } from "./uniformBuffer.js";
|
|
|
34
35
|
import { compileRenderPipeline, type CompileRenderPipelineDescription } from "./renderPipeline.js";
|
|
35
36
|
import { BufferUsage, ShaderStage } from "./webgpuFlags.js";
|
|
36
37
|
|
|
37
|
-
// ---------------------------------------------------------------------------
|
|
38
|
-
// Helpers
|
|
39
|
-
// ---------------------------------------------------------------------------
|
|
40
|
-
|
|
41
|
-
function isIndexFormat(fmt: string): boolean {
|
|
42
|
-
return fmt === "uint16" || fmt === "uint32";
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function vertexFormatStride(fmt: GPUVertexFormat): number {
|
|
46
|
-
switch (fmt) {
|
|
47
|
-
case "float32": return 4;
|
|
48
|
-
case "float32x2": return 8;
|
|
49
|
-
case "float32x3": return 12;
|
|
50
|
-
case "float32x4": return 16;
|
|
51
|
-
case "uint32": case "sint32": return 4;
|
|
52
|
-
case "uint32x2": case "sint32x2": return 8;
|
|
53
|
-
case "uint32x3": case "sint32x3": return 12;
|
|
54
|
-
case "uint32x4": case "sint32x4": return 16;
|
|
55
|
-
case "uint16x2": case "sint16x2": case "float16x2": return 4;
|
|
56
|
-
case "uint16x4": case "sint16x4": case "float16x4": return 8;
|
|
57
|
-
case "uint8x2": case "sint8x2": case "unorm8x2": case "snorm8x2": return 2;
|
|
58
|
-
case "uint8x4": case "sint8x4": case "unorm8x4": case "snorm8x4": return 4;
|
|
59
|
-
default: throw new Error(`vertexFormatStride: unsupported format ${fmt}`);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
38
|
interface VertexBindingInfo {
|
|
64
39
|
readonly name: string;
|
|
65
40
|
readonly slot: number;
|
|
@@ -371,10 +346,10 @@ export class PreparedRenderObject {
|
|
|
371
346
|
private readonly device: GPUDevice,
|
|
372
347
|
private readonly vertexBindings: readonly VertexBindingInfo[],
|
|
373
348
|
private readonly vertexBuffers: ReadonlyMap<string, AdaptiveResource<GPUBuffer>>,
|
|
374
|
-
private readonly vertexViews: ReadonlyMap<string,
|
|
349
|
+
private readonly vertexViews: ReadonlyMap<string, BufferView>,
|
|
375
350
|
private readonly indexBuffer: AdaptiveResource<GPUBuffer> | undefined,
|
|
376
351
|
private readonly indexFormat: GPUIndexFormat | undefined,
|
|
377
|
-
private readonly indices:
|
|
352
|
+
private readonly indices: BufferView | undefined,
|
|
378
353
|
groups: readonly GroupDesc[],
|
|
379
354
|
private readonly drawCall: aval<import("../core/index.js").DrawCall>,
|
|
380
355
|
pipelineCtx: PipelineBuildContext,
|
|
@@ -479,15 +454,12 @@ export class PreparedRenderObject {
|
|
|
479
454
|
for (const vb of this.vertexBindings) {
|
|
480
455
|
const res = this.vertexBuffers.get(vb.name);
|
|
481
456
|
if (res === undefined) throw new Error(`PreparedRenderObject.record: missing vertex buffer for "${vb.name}"`);
|
|
482
|
-
const
|
|
483
|
-
const view = viewAval !== undefined ? viewAval.getValue(token) : undefined;
|
|
457
|
+
const view = this.vertexViews.get(vb.name);
|
|
484
458
|
const buf = res.getValue(token);
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
//
|
|
488
|
-
|
|
489
|
-
// buffer for any non-zero offset; let WebGPU default.
|
|
490
|
-
pass.setVertexBuffer(vb.slot, buf, view.offset);
|
|
459
|
+
const offset = view !== undefined ? BufferView.offsetOf(view) : 0;
|
|
460
|
+
if (offset > 0) {
|
|
461
|
+
// size omitted → WebGPU uses (buffer.size − offset).
|
|
462
|
+
pass.setVertexBuffer(vb.slot, buf, offset);
|
|
491
463
|
} else {
|
|
492
464
|
pass.setVertexBuffer(vb.slot, buf);
|
|
493
465
|
}
|
|
@@ -495,8 +467,8 @@ export class PreparedRenderObject {
|
|
|
495
467
|
|
|
496
468
|
if (this.indexBuffer !== undefined && this.indices !== undefined && this.indexFormat !== undefined) {
|
|
497
469
|
const buf = this.indexBuffer.getValue(token);
|
|
498
|
-
|
|
499
|
-
pass.setIndexBuffer(buf, this.indexFormat,
|
|
470
|
+
// size omitted → WebGPU uses (buffer.size − offset).
|
|
471
|
+
pass.setIndexBuffer(buf, this.indexFormat, BufferView.offsetOf(this.indices));
|
|
500
472
|
}
|
|
501
473
|
|
|
502
474
|
const dc = this.drawCall.getValue(token);
|
|
@@ -531,7 +503,7 @@ export function prepareRenderObject(
|
|
|
531
503
|
opts: PrepareRenderObjectOptions = {},
|
|
532
504
|
): PreparedRenderObject {
|
|
533
505
|
const iface = effect.interface;
|
|
534
|
-
|
|
506
|
+
let vertexBindings = vertexBindingsFor(iface);
|
|
535
507
|
|
|
536
508
|
// The set of attribute names is fixed structurally; only the
|
|
537
509
|
// per-attribute BufferView avals are reactive. We resolve names →
|
|
@@ -539,9 +511,28 @@ export function prepareRenderObject(
|
|
|
539
511
|
const vertexMap = obj.vertexAttributes;
|
|
540
512
|
const instanceMap = obj.instanceAttributes ?? emptyAttrMap();
|
|
541
513
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
514
|
+
// Per-attribute resolution + grouping. Multiple attributes can
|
|
515
|
+
// share one underlying GPUBuffer slot when they all point at the
|
|
516
|
+
// same `aval<IBuffer>` (by reference identity) with the same
|
|
517
|
+
// stride + stepMode (the auto-instancing rewrite emits 4 vec4
|
|
518
|
+
// column attributes per M44 instance trafo — without packing
|
|
519
|
+
// we'd hit `maxVertexBuffers` (8) for any moderately rich
|
|
520
|
+
// instancing scope). Each attribute's `view.offset` becomes its
|
|
521
|
+
// shader-location offset within the shared layout.
|
|
522
|
+
//
|
|
523
|
+
// BufferView is now structural-eager (elementType/offset/stride),
|
|
524
|
+
// so no `force()` is needed to discover layout — the attribute
|
|
525
|
+
// grouping reads the plain fields directly.
|
|
526
|
+
interface ResolvedBinding {
|
|
527
|
+
readonly name: string;
|
|
528
|
+
readonly shaderLocation: number;
|
|
529
|
+
readonly stepMode: GPUVertexStepMode;
|
|
530
|
+
readonly stride: number;
|
|
531
|
+
readonly offset: number;
|
|
532
|
+
readonly format: GPUVertexFormat;
|
|
533
|
+
readonly view: BufferView;
|
|
534
|
+
}
|
|
535
|
+
const resolved: ResolvedBinding[] = [];
|
|
545
536
|
for (const vb of vertexBindings) {
|
|
546
537
|
const fromVertex = vertexMap.tryFind(vb.name);
|
|
547
538
|
const fromInstance = instanceMap.tryFind(vb.name);
|
|
@@ -551,61 +542,106 @@ export function prepareRenderObject(
|
|
|
551
542
|
const isInstance = fromVertex === undefined && fromInstance !== undefined;
|
|
552
543
|
const stepMode: GPUVertexStepMode = isInstance ? "instance" : "vertex";
|
|
553
544
|
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
const initialView = viewAval.force();
|
|
564
|
-
const explicitZeroStride = initialView.stride === 0;
|
|
565
|
-
// The buffer's own `format` (e.g. "float32x3" for a packed-Vec3
|
|
566
|
-
// position stream) is the source of truth — use it whenever
|
|
567
|
-
// present so the layout matches what's actually in memory.
|
|
568
|
-
// WebGPU auto-pads missing components when the shader declares a
|
|
569
|
-
// wider vector (vec4 input fed by Float32x3 → w=1).
|
|
570
|
-
const viewFormat = initialView.format as GPUVertexFormat | undefined;
|
|
571
|
-
const format = (viewFormat !== undefined && !isIndexFormat(viewFormat))
|
|
572
|
-
? viewFormat
|
|
573
|
-
: vb.format;
|
|
574
|
-
const stride = explicitZeroStride
|
|
575
|
-
? 0
|
|
576
|
-
: (initialView.stride > 0
|
|
577
|
-
? initialView.stride
|
|
578
|
-
: vertexFormatStride(format));
|
|
579
|
-
|
|
580
|
-
const bufAval = viewAval.map(view => view.buffer);
|
|
581
|
-
const res = prepareAdaptiveBuffer(device, bufAval, {
|
|
582
|
-
usage: BufferUsage.VERTEX,
|
|
583
|
-
...(opts.label !== undefined ? { label: `${opts.label}.${vb.name}` } : {}),
|
|
545
|
+
const view = (fromVertex ?? fromInstance)!;
|
|
546
|
+
const format = ElementType.toVertexFormat(view.elementType, view.normalized ?? false);
|
|
547
|
+
const stride = BufferView.strideOf(view);
|
|
548
|
+
const offset = BufferView.offsetOf(view);
|
|
549
|
+
resolved.push({
|
|
550
|
+
name: vb.name,
|
|
551
|
+
shaderLocation: vb.slot,
|
|
552
|
+
stepMode, stride, offset,
|
|
553
|
+
format, view,
|
|
584
554
|
});
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Group resolved bindings by (IBuffer-aval identity, stride, stepMode).
|
|
558
|
+
// The aval reference is the natural identity now that BufferView
|
|
559
|
+
// owns its `aval<IBuffer>` directly.
|
|
560
|
+
type GroupKey = string;
|
|
561
|
+
interface BindingGroup {
|
|
562
|
+
readonly key: GroupKey;
|
|
563
|
+
readonly slot: number; // GPU vertex-buffer slot index
|
|
564
|
+
readonly stride: number;
|
|
565
|
+
readonly stepMode: GPUVertexStepMode;
|
|
566
|
+
readonly view: BufferView;
|
|
567
|
+
readonly bufferRes: AdaptiveResource<GPUBuffer>;
|
|
568
|
+
readonly members: ResolvedBinding[];
|
|
569
|
+
}
|
|
570
|
+
const bufferIds = new WeakMap<object, number>();
|
|
571
|
+
let nextBufId = 0;
|
|
572
|
+
const idOf = (b: object): number => {
|
|
573
|
+
let n = bufferIds.get(b);
|
|
574
|
+
if (n === undefined) { n = nextBufId++; bufferIds.set(b, n); }
|
|
575
|
+
return n;
|
|
576
|
+
};
|
|
577
|
+
const vbGroups = new Map<GroupKey, BindingGroup>();
|
|
578
|
+
let nextSlot = 0;
|
|
579
|
+
const vertexBuffers = new Map<string, AdaptiveResource<GPUBuffer>>();
|
|
580
|
+
const vertexViews = new Map<string, BufferView>();
|
|
581
|
+
for (const r of resolved) {
|
|
582
|
+
const bufId = idOf(r.view.buffer as unknown as object);
|
|
583
|
+
const key = `${bufId}|${r.stride}|${r.stepMode}`;
|
|
584
|
+
let group = vbGroups.get(key);
|
|
585
|
+
if (!group) {
|
|
586
|
+
const bufferRes = prepareAdaptiveBuffer(device, r.view.buffer, {
|
|
587
|
+
usage: BufferUsage.VERTEX,
|
|
588
|
+
...(opts.label !== undefined ? { label: `${opts.label}.${r.name}` } : {}),
|
|
589
|
+
});
|
|
590
|
+
group = {
|
|
591
|
+
key, slot: nextSlot++, stride: r.stride, stepMode: r.stepMode,
|
|
592
|
+
view: r.view, bufferRes, members: [],
|
|
593
|
+
};
|
|
594
|
+
vbGroups.set(key, group);
|
|
595
|
+
}
|
|
596
|
+
group.members.push(r);
|
|
597
|
+
vertexBuffers.set(r.name, group.bufferRes);
|
|
598
|
+
vertexViews.set(r.name, r.view);
|
|
599
|
+
}
|
|
600
|
+
const vertexLayouts: GPUVertexBufferLayout[] = [];
|
|
601
|
+
for (const g of vbGroups.values()) {
|
|
602
|
+
vertexLayouts[g.slot] = {
|
|
603
|
+
arrayStride: g.stride,
|
|
604
|
+
stepMode: g.stepMode,
|
|
605
|
+
attributes: g.members.map((m) => ({
|
|
606
|
+
shaderLocation: m.shaderLocation,
|
|
607
|
+
offset: m.offset,
|
|
608
|
+
format: m.format,
|
|
609
|
+
})),
|
|
591
610
|
};
|
|
592
611
|
}
|
|
612
|
+
// The IR-side `vertexBindings.slot` was the per-attribute shader
|
|
613
|
+
// location. The runtime needs the GPU buffer-slot index. Build a
|
|
614
|
+
// parallel array keyed by binding name so the per-frame
|
|
615
|
+
// `setVertexBuffer` loop calls the right slot once per group.
|
|
616
|
+
const groupSlotByBindingName = new Map<string, number>();
|
|
617
|
+
for (const g of vbGroups.values()) {
|
|
618
|
+
for (const m of g.members) groupSlotByBindingName.set(m.name, g.slot);
|
|
619
|
+
}
|
|
620
|
+
// Build the unique-per-slot binding list the prepared object uses
|
|
621
|
+
// at draw time. Pick the first member of each group to represent
|
|
622
|
+
// the buffer; per-attribute view offsets live in the layout.
|
|
623
|
+
const drawTimeBindings: VertexBindingInfo[] = [];
|
|
624
|
+
for (const g of vbGroups.values()) {
|
|
625
|
+
const head = g.members[0]!;
|
|
626
|
+
drawTimeBindings.push({
|
|
627
|
+
name: head.name,
|
|
628
|
+
slot: g.slot,
|
|
629
|
+
format: head.format,
|
|
630
|
+
byteSize: 0,
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
vertexBindings = drawTimeBindings;
|
|
593
634
|
|
|
594
|
-
// Index buffer — `
|
|
635
|
+
// Index buffer — `elementType` (u16 / u32) determines `GPUIndexFormat`.
|
|
636
|
+
// Structural fields are eager on BufferView, so no force needed.
|
|
595
637
|
let indexBuffer: AdaptiveResource<GPUBuffer> | undefined;
|
|
596
638
|
let indexFormat: GPUIndexFormat | undefined;
|
|
597
639
|
if (obj.indices !== undefined) {
|
|
598
|
-
|
|
599
|
-
indexBuffer = prepareAdaptiveBuffer(device, bufAval, {
|
|
640
|
+
indexBuffer = prepareAdaptiveBuffer(device, obj.indices.buffer, {
|
|
600
641
|
usage: BufferUsage.INDEX,
|
|
601
642
|
...(opts.label !== undefined ? { label: `${opts.label}.indices` } : {}),
|
|
602
643
|
});
|
|
603
|
-
|
|
604
|
-
// BufferView aval is expected to settle on a stable format —
|
|
605
|
-
// changing index format would require a fresh PreparedRO.
|
|
606
|
-
const initial = obj.indices.force();
|
|
607
|
-
indexFormat = initial.indexFormat
|
|
608
|
-
?? (initial.format === "uint16" ? "uint16" : "uint32");
|
|
644
|
+
indexFormat = ElementType.toIndexFormat(obj.indices.elementType);
|
|
609
645
|
}
|
|
610
646
|
|
|
611
647
|
const slotOf = (e: { group: number; slot: number }) => e.slot;
|
|
@@ -695,8 +731,8 @@ export function prepareRenderObject(
|
|
|
695
731
|
);
|
|
696
732
|
}
|
|
697
733
|
|
|
698
|
-
function emptyAttrMap(): HashMap<string,
|
|
699
|
-
return HashMap.empty<string,
|
|
734
|
+
function emptyAttrMap(): HashMap<string, BufferView> {
|
|
735
|
+
return HashMap.empty<string, BufferView>();
|
|
700
736
|
}
|
|
701
737
|
|
|
702
738
|
/**
|
package/src/window/loop.ts
CHANGED
|
@@ -1,20 +1,44 @@
|
|
|
1
|
-
// runFrame —
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// their `IRenderTask` here.
|
|
1
|
+
// runFrame — adaptivity-driven render loop. Mirrors Aardvark.Rendering's
|
|
2
|
+
// approach: an AVal wraps the user's frame callback, the frame's
|
|
3
|
+
// dependency set comes from whatever the callback reads via the
|
|
4
|
+
// adaptive `token`, and a marking callback on the wrapper schedules
|
|
5
|
+
// the next `requestAnimationFrame`. Idle scenes (no avals fired
|
|
6
|
+
// since last render) consume zero CPU per rAF.
|
|
8
7
|
//
|
|
9
|
-
//
|
|
8
|
+
// The standard time-ticking pattern (which the wombat.dom
|
|
9
|
+
// `<RenderControl>` follows) is:
|
|
10
|
+
// 1. The user's frame callback calls `task.run(token)` which
|
|
11
|
+
// reads its inputs (uniforms, alists, …) via the token.
|
|
12
|
+
// Those reads register as inputs to the wrapping AVal.
|
|
13
|
+
// 2. After `task.run`, the callback ticks a global `time` cval
|
|
14
|
+
// inside a `transact(...)`.
|
|
15
|
+
// 3. If anything in the scene (e.g. an `OrbitController`'s
|
|
16
|
+
// damping aval, a custom animation aval) reads `time`, the
|
|
17
|
+
// mark cascades into the wrapper → marking callback fires →
|
|
18
|
+
// next rAF scheduled. If *nothing* depends on time, the mark
|
|
19
|
+
// dies in midair and the loop sleeps.
|
|
20
|
+
// 4. Canvas resize / explicit cval edits / scene-graph deltas
|
|
21
|
+
// mark their respective avals → cascade → wake-up.
|
|
22
|
+
//
|
|
23
|
+
// Returns a `stop()` to cancel the loop and clear the subscription.
|
|
10
24
|
|
|
11
|
-
import type { AdaptiveToken } from "@aardworx/wombat.adaptive";
|
|
12
|
-
import {
|
|
25
|
+
import type { aval, AdaptiveToken, IDisposable } from "@aardworx/wombat.adaptive";
|
|
26
|
+
import { AVal, transact } from "@aardworx/wombat.adaptive";
|
|
13
27
|
import type { CanvasAttachment } from "./canvas.js";
|
|
14
28
|
|
|
15
29
|
export interface RunFrameOptions {
|
|
16
30
|
/** Stop after N frames. Useful for tests + finite animations. */
|
|
17
31
|
readonly maxFrames?: number;
|
|
32
|
+
/**
|
|
33
|
+
* Hook called after each frame's eval completes — typically used
|
|
34
|
+
* to tick a `time` cval (so any aval that depends on time gets
|
|
35
|
+
* marked AFTER the frame, waking the next rAF). Fired in a fresh
|
|
36
|
+
* transaction. Returning without ticking (`onAfterFrame: () => {}`,
|
|
37
|
+
* or omitting) means the loop won't advance unless something else
|
|
38
|
+
* marks an aval that the frame reads. That's the correct idle
|
|
39
|
+
* behavior.
|
|
40
|
+
*/
|
|
41
|
+
readonly onAfterFrame?: () => void;
|
|
18
42
|
}
|
|
19
43
|
|
|
20
44
|
export type FrameCallback = (token: AdaptiveToken, info: FrameInfo) => void;
|
|
@@ -34,29 +58,68 @@ export function runFrame(
|
|
|
34
58
|
let frameNo = 0;
|
|
35
59
|
let lastTime = performance.now();
|
|
36
60
|
let rafId = 0;
|
|
61
|
+
let pending = false;
|
|
37
62
|
|
|
38
|
-
|
|
39
|
-
|
|
63
|
+
// Wrap the frame callback in an AVal so the adaptive system tracks
|
|
64
|
+
// its dependency set. Each `force()` re-reads the inputs and runs
|
|
65
|
+
// the frame; subsequent input marks fire the marking callback.
|
|
66
|
+
const renderAval: aval<number> = AVal.custom((token) => {
|
|
67
|
+
const now = performance.now();
|
|
40
68
|
const delta = now - lastTime;
|
|
41
69
|
lastTime = now;
|
|
42
70
|
attachment.markFrame();
|
|
43
71
|
try {
|
|
44
|
-
frame(
|
|
72
|
+
frame(token, { frame: frameNo, timestampMs: now, deltaMs: delta });
|
|
45
73
|
} catch (e) {
|
|
46
74
|
stopped = true;
|
|
47
75
|
console.error("runFrame: error in frame callback", e);
|
|
48
76
|
throw e;
|
|
49
77
|
}
|
|
50
78
|
frameNo++;
|
|
51
|
-
|
|
52
|
-
|
|
79
|
+
return frameNo;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const tick = (): void => {
|
|
83
|
+
if (stopped) return;
|
|
84
|
+
pending = false;
|
|
85
|
+
// The single force-on-render-path. Pulls `renderAval`, which
|
|
86
|
+
// re-evaluates the user's frame callback (making it up-to-date
|
|
87
|
+
// for the next mark). Adaptivity-driven equivalent of "run one
|
|
88
|
+
// frame on this rAF."
|
|
89
|
+
renderAval.force(/* allow-force */);
|
|
90
|
+
// Post-frame hook in a fresh transaction. Marks fired here
|
|
91
|
+
// propagate AFTER the eval, so they actually reach
|
|
92
|
+
// `renderAval`'s marking callback and wake the next rAF.
|
|
93
|
+
// Inside the eval the same mark would race the
|
|
94
|
+
// `outOfDate=false` reset and animation would stall after one
|
|
95
|
+
// frame.
|
|
96
|
+
if (opts.onAfterFrame !== undefined) {
|
|
97
|
+
transact(opts.onAfterFrame);
|
|
98
|
+
}
|
|
99
|
+
if (opts.maxFrames !== undefined && frameNo >= opts.maxFrames) {
|
|
100
|
+
stopped = true;
|
|
101
|
+
sub.dispose();
|
|
102
|
+
}
|
|
53
103
|
};
|
|
54
104
|
|
|
105
|
+
const sub: IDisposable = (renderAval as unknown as {
|
|
106
|
+
addMarkingCallback(cb: () => void): IDisposable;
|
|
107
|
+
}).addMarkingCallback(() => {
|
|
108
|
+
if (pending || stopped) return;
|
|
109
|
+
pending = true;
|
|
110
|
+
rafId = requestAnimationFrame(tick);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Initial render. Schedules subsequent frames only when adaptive
|
|
114
|
+
// marks reach `renderAval` (via the marking callback above).
|
|
115
|
+
pending = true;
|
|
55
116
|
rafId = requestAnimationFrame(tick);
|
|
117
|
+
|
|
56
118
|
return {
|
|
57
119
|
stop() {
|
|
58
120
|
stopped = true;
|
|
59
121
|
cancelAnimationFrame(rafId);
|
|
122
|
+
sub.dispose();
|
|
60
123
|
},
|
|
61
124
|
};
|
|
62
125
|
}
|