@aardworx/wombat.rendering 0.20.2 → 0.21.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.
- package/dist/runtime/heapAdapter.d.ts +1 -1
- package/dist/runtime/heapAdapter.d.ts.map +1 -1
- package/dist/runtime/heapAdapter.js +13 -0
- package/dist/runtime/heapAdapter.js.map +1 -1
- package/dist/runtime/heapDecoder.d.ts +4 -0
- package/dist/runtime/heapDecoder.d.ts.map +1 -1
- package/dist/runtime/heapDecoder.js +44 -3
- package/dist/runtime/heapDecoder.js.map +1 -1
- package/dist/runtime/heapEffect.d.ts +1 -1
- package/dist/runtime/heapEffect.js +1 -1
- package/dist/runtime/heapEffect.js.map +1 -1
- package/dist/runtime/heapEffectIR.d.ts.map +1 -1
- package/dist/runtime/heapEffectIR.js +172 -11
- package/dist/runtime/heapEffectIR.js.map +1 -1
- package/dist/runtime/heapScene.d.ts +1 -1
- package/dist/runtime/heapScene.d.ts.map +1 -1
- package/dist/runtime/heapScene.js +33 -19
- package/dist/runtime/heapScene.js.map +1 -1
- package/dist/runtime/hybridScene.d.ts.map +1 -1
- package/dist/runtime/hybridScene.js +11 -2
- package/dist/runtime/hybridScene.js.map +1 -1
- package/dist/runtime/textureAtlas/atlasPool.d.ts +6 -2
- package/dist/runtime/textureAtlas/atlasPool.d.ts.map +1 -1
- package/dist/runtime/textureAtlas/atlasPool.js +36 -1
- package/dist/runtime/textureAtlas/atlasPool.js.map +1 -1
- package/package.json +1 -1
- package/src/runtime/heapAdapter.ts +18 -1
- package/src/runtime/heapDecoder.ts +45 -3
- package/src/runtime/heapEffect.ts +1 -1
- package/src/runtime/heapEffectIR.ts +180 -11
- package/src/runtime/heapScene.ts +15 -2
- package/src/runtime/hybridScene.ts +10 -2
- package/src/runtime/textureAtlas/atlasPool.ts +39 -5
|
@@ -22,7 +22,7 @@ import { isInlineHeaderField } from "./heapEffect.js";
|
|
|
22
22
|
import type { Effect, CompileOptions } from "@aardworx/wombat.shader";
|
|
23
23
|
import { substituteInputsInStage, readInputs, mapExpr, mapStmt, liftReturns, uniformsToInputs, resolveHoles } from "@aardworx/wombat.shader/passes";
|
|
24
24
|
import type { HoleValue } from "@aardworx/wombat.shader/passes";
|
|
25
|
-
import { synthesizeHeapDecoderModule } from "./heapDecoder.js";
|
|
25
|
+
import { synthesizeHeapDecoderModule, HEAP_FS_DRAWIDX, HEAP_FS_INSTID } from "./heapDecoder.js";
|
|
26
26
|
import {
|
|
27
27
|
type Module, type Expr, type Stmt, type Type, type ValueDef,
|
|
28
28
|
type EntryDef, type EntryParameter, type ParamDecoration,
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
generateAtlasBindings, generateAtlasSwitch, generateAtlasPrelude,
|
|
34
34
|
HEAP_PERSIST_VERSION, persistKey, lsLoad, lsStore,
|
|
35
35
|
type BucketLayout,
|
|
36
|
+
type DrawHeaderField,
|
|
36
37
|
} from "./heapEffect.js";
|
|
37
38
|
import type { IntrinsicRef } from "@aardworx/wombat.shader/ir";
|
|
38
39
|
|
|
@@ -348,6 +349,23 @@ function addInterstageParams(
|
|
|
348
349
|
return { ...m, values };
|
|
349
350
|
}
|
|
350
351
|
|
|
352
|
+
/**
|
|
353
|
+
* Declare extra FRAGMENT inputs, leaving the vertex side alone. The decoder
|
|
354
|
+
* path already writes its carrier on the VS; the FS just needs the matching
|
|
355
|
+
* declaration to read it.
|
|
356
|
+
*/
|
|
357
|
+
function addFsInputs(m: Module, extras: readonly EntryParameter[]): Module {
|
|
358
|
+
if (extras.length === 0) return m;
|
|
359
|
+
const values = m.values.map((v): typeof v => {
|
|
360
|
+
if (v.kind !== "Entry" || v.entry.stage !== "fragment") return v;
|
|
361
|
+
const have = new Set(v.entry.inputs.map(p => p.name));
|
|
362
|
+
const add = extras.filter(p => !have.has(p.name));
|
|
363
|
+
if (add.length === 0) return v;
|
|
364
|
+
return { ...v, entry: { ...v.entry, inputs: [...v.entry.inputs, ...add] } };
|
|
365
|
+
});
|
|
366
|
+
return { ...m, values };
|
|
367
|
+
}
|
|
368
|
+
|
|
351
369
|
/**
|
|
352
370
|
* Prepend `stmts` to every VS entry's body. Flatten when the body is
|
|
353
371
|
* already a `Sequential` — CSE numbers `_cse0..N` per Sequential, so
|
|
@@ -407,7 +425,15 @@ function wgslTypeFromHeapField(wgslType: string): Type {
|
|
|
407
425
|
* emit `applyFamilyMemberFsShape` rewires the FS entry signature to
|
|
408
426
|
* surface them as plain u32 fn parameters.
|
|
409
427
|
*/
|
|
410
|
-
function rewriteFsUniformsDirect(
|
|
428
|
+
function rewriteFsUniformsDirect(
|
|
429
|
+
m: Module,
|
|
430
|
+
layout: BucketLayout,
|
|
431
|
+
// Where the FS gets the per-draw header index from. `family-member` receives
|
|
432
|
+
// it as a plain u32 fn parameter (a Var); `standalone` reads it from the one
|
|
433
|
+
// flat varying it threads. Everything else about the direct load is identical.
|
|
434
|
+
drawIdxExpr: Expr = { kind: "Var", var: { name: "heap_drawIdx", type: Tu32, mutable: false } } as Expr,
|
|
435
|
+
instIdExpr: Expr = { kind: "Var", var: { name: "instId", type: Tu32, mutable: false } } as Expr,
|
|
436
|
+
): Module {
|
|
411
437
|
const used = new Set<string>();
|
|
412
438
|
for (const v of m.values) {
|
|
413
439
|
if (v.kind !== "Entry" || v.entry.stage !== "fragment") continue;
|
|
@@ -418,8 +444,6 @@ function rewriteFsUniformsDirect(m: Module, layout: BucketLayout): Module {
|
|
|
418
444
|
if (used.size === 0) return m;
|
|
419
445
|
|
|
420
446
|
const fieldByName = new Map(layout.drawHeaderFields.map(f => [f.name, f]));
|
|
421
|
-
const drawIdxExpr: Expr = { kind: "Var", var: { name: "heap_drawIdx", type: Tu32, mutable: false } } as Expr;
|
|
422
|
-
const instIdExpr: Expr = { kind: "Var", var: { name: "instId", type: Tu32, mutable: false } } as Expr;
|
|
423
447
|
const fsSubst = new Map<string, Expr>();
|
|
424
448
|
for (const name of used) {
|
|
425
449
|
const f = fieldByName.get(name);
|
|
@@ -445,7 +469,11 @@ function rewriteFsUniformsDirect(m: Module, layout: BucketLayout): Module {
|
|
|
445
469
|
* `heap_drawIdx`. The wrapper-supplied `heap_drawIdx` u32 fn parameter
|
|
446
470
|
* carries the per-draw header index.
|
|
447
471
|
*/
|
|
448
|
-
function rewriteFsAtlasTexturesDirect(
|
|
472
|
+
function rewriteFsAtlasTexturesDirect(
|
|
473
|
+
m: Module,
|
|
474
|
+
layout: BucketLayout,
|
|
475
|
+
drawIdxExprIn: Expr = { kind: "Var", var: { name: "heap_drawIdx", type: Tu32, mutable: false } } as Expr,
|
|
476
|
+
): Module {
|
|
449
477
|
if (layout.atlasTextureBindings.size === 0) return m;
|
|
450
478
|
const used = new Set<string>();
|
|
451
479
|
for (const v of m.values) {
|
|
@@ -466,7 +494,7 @@ function rewriteFsAtlasTexturesDirect(m: Module, layout: BucketLayout): Module {
|
|
|
466
494
|
inner.set(sub, byteOffsetOf(f.byteOffset));
|
|
467
495
|
}
|
|
468
496
|
|
|
469
|
-
const drawIdxExpr: Expr =
|
|
497
|
+
const drawIdxExpr: Expr = drawIdxExprIn;
|
|
470
498
|
const stride = layout.strideU32;
|
|
471
499
|
const headerU32At = (offU32: number): Expr =>
|
|
472
500
|
item(headersU32, add(mul(drawIdxExpr, constU32(stride), Tu32), constU32(offU32), Tu32), Tu32);
|
|
@@ -499,6 +527,78 @@ function rewriteFsAtlasTexturesDirect(m: Module, layout: BucketLayout): Module {
|
|
|
499
527
|
}));
|
|
500
528
|
}
|
|
501
529
|
|
|
530
|
+
/**
|
|
531
|
+
* STANDALONE FS heap access — one varying, not N.
|
|
532
|
+
*
|
|
533
|
+
* The old standalone path threaded a VS→FS varying per FS-used uniform AND
|
|
534
|
+
* FOUR per atlas texture (`_h_<t>{PageRef,FormatBits,Origin,Size}`). WebGPU
|
|
535
|
+
* caps inter-stage variables at 16 locations, so a shader with a handful of
|
|
536
|
+
* per-draw uniforms and a texture ran into the ceiling — and blowing it does
|
|
537
|
+
* not fail loudly: the pipeline is created INVALID, which poisons the whole
|
|
538
|
+
* command buffer, so the entire pass silently disappears. It made the varying
|
|
539
|
+
* budget a design constraint on every shader written against the heap.
|
|
540
|
+
*
|
|
541
|
+
* `family-member` never had this problem: it loads uniforms and atlas headers
|
|
542
|
+
* straight out of the heap arena in the FS, keyed by the draw index. Standalone
|
|
543
|
+
* now does the same — it just has to carry the draw index itself, so it threads
|
|
544
|
+
* exactly ONE flat u32 varying (`_h_drawIdx`), plus `_iidx` when a per-instance
|
|
545
|
+
* uniform is actually read from the FS. Varying count is now O(1) in the number
|
|
546
|
+
* of uniforms and textures.
|
|
547
|
+
*
|
|
548
|
+
* The trade is per-fragment storage reads instead of per-vertex ones. They are
|
|
549
|
+
* scalar, flat, and uniform across a draw (so they hit cache), and this is the
|
|
550
|
+
* path `family-member` has been running in production all along.
|
|
551
|
+
*/
|
|
552
|
+
function rewriteFsHeapDirectStandalone(m: Module, layout: BucketLayout): Module {
|
|
553
|
+
// What does the FS actually touch?
|
|
554
|
+
const usedUniforms = new Set<string>();
|
|
555
|
+
const usedAtlas = new Set<string>();
|
|
556
|
+
for (const v of m.values) {
|
|
557
|
+
if (v.kind !== "Entry" || v.entry.stage !== "fragment") continue;
|
|
558
|
+
for (const sn of readInputs(v.entry.body).values()) {
|
|
559
|
+
if (sn.scope === "Uniform") usedUniforms.add(sn.name);
|
|
560
|
+
}
|
|
561
|
+
visitStmtExprs(v.entry.body, e => {
|
|
562
|
+
const hit = isAtlasSampleCall(e, layout.atlasTextureBindings);
|
|
563
|
+
if (hit !== null) usedAtlas.add(hit.name);
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
if (usedUniforms.size === 0 && usedAtlas.size === 0) return m;
|
|
567
|
+
|
|
568
|
+
// Thread the carrier(s). `_iidx` only when a per-instance uniform is read —
|
|
569
|
+
// otherwise it is a varying nobody needs.
|
|
570
|
+
const needsIidx = [...usedUniforms].some(n => layout.perInstanceUniforms.has(n));
|
|
571
|
+
const threadParams: EntryParameter[] = [{
|
|
572
|
+
name: "_h_drawIdx", type: Tu32, semantic: "_h_drawIdx",
|
|
573
|
+
decorations: [{ kind: "Interpolation", mode: "flat" } as ParamDecoration],
|
|
574
|
+
}];
|
|
575
|
+
const vsWrites: Stmt[] = [{
|
|
576
|
+
kind: "WriteOutput", name: "_h_drawIdx",
|
|
577
|
+
value: { kind: "Expr", value: drawIdxExprFor(layout) },
|
|
578
|
+
}];
|
|
579
|
+
if (needsIidx) {
|
|
580
|
+
threadParams.push({
|
|
581
|
+
name: "_iidx", type: Tu32, semantic: "_iidx",
|
|
582
|
+
decorations: [{ kind: "Interpolation", mode: "flat" } as ParamDecoration],
|
|
583
|
+
});
|
|
584
|
+
vsWrites.push({
|
|
585
|
+
kind: "WriteOutput", name: "_iidx",
|
|
586
|
+
value: { kind: "Expr", value: readScope("Builtin", "instance_index", Tu32) },
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const fsDrawIdx = readScope("Input", "_h_drawIdx", Tu32);
|
|
591
|
+
const fsInstId = readScope("Input", "_iidx", Tu32);
|
|
592
|
+
|
|
593
|
+
let out = m;
|
|
594
|
+
out = addInterstageParams(out, threadParams);
|
|
595
|
+
out = prependVsBodyStmts(out, vsWrites);
|
|
596
|
+
// Same loaders the family path uses — only the source of the draw index differs.
|
|
597
|
+
out = rewriteFsAtlasTexturesDirect(out, layout, fsDrawIdx);
|
|
598
|
+
out = rewriteFsUniformsDirect(out, layout, fsDrawIdx, fsInstId);
|
|
599
|
+
return out;
|
|
600
|
+
}
|
|
601
|
+
|
|
502
602
|
/**
|
|
503
603
|
* FS uniform threading: thread `_h_<name>Ref` (and `_iidx`, when
|
|
504
604
|
* needed) varyings from VS to FS, and rewrite FS `ReadInput("Uniform",
|
|
@@ -900,8 +1000,11 @@ export function compileHeapEffectIR(
|
|
|
900
1000
|
combined = rewriteFsAtlasTexturesDirect(combined, layout);
|
|
901
1001
|
combined = rewriteFsUniformsDirect(combined, layout);
|
|
902
1002
|
} else {
|
|
903
|
-
|
|
904
|
-
|
|
1003
|
+
// Standalone reads the heap directly too — it just carries the draw index
|
|
1004
|
+
// in ONE flat varying instead of receiving it as a parameter. (The old
|
|
1005
|
+
// per-uniform / 4-per-texture varying threading is gone: it hit WebGPU's
|
|
1006
|
+
// 16-location inter-stage cap and silently invalidated whole pipelines.)
|
|
1007
|
+
combined = rewriteFsHeapDirectStandalone(combined, layout);
|
|
905
1008
|
}
|
|
906
1009
|
// Add heap storage buffers as bindings.
|
|
907
1010
|
combined = { ...combined, values: [...heapStorageBufferDecls(), ...combined.values] };
|
|
@@ -974,6 +1077,67 @@ export function compileHeapEffectIR(
|
|
|
974
1077
|
* No post-emit string mangling — `applyMegacallToEmittedVs` /
|
|
975
1078
|
* `applyFamilyMemberShape` are gone for this path.
|
|
976
1079
|
*/
|
|
1080
|
+
/**
|
|
1081
|
+
* FS heap access for the DECODER path: read per-draw uniforms and atlas-texture
|
|
1082
|
+
* headers directly out of the arena, keyed by the draw index the decoder put on
|
|
1083
|
+
* the carrier (`_h_drawIdx`), instead of reading values the decoder pushed
|
|
1084
|
+
* across as one varying each.
|
|
1085
|
+
*
|
|
1086
|
+
* Only `uniform-ref` fields are substituted. `attribute-ref` fields are
|
|
1087
|
+
* per-VERTEX data — they must keep flowing through the carrier, and they were
|
|
1088
|
+
* renamed into `Input` scope by `uniformsToInputs` just like the uniforms, so
|
|
1089
|
+
* the substitution has to be name-driven, not scope-driven.
|
|
1090
|
+
*/
|
|
1091
|
+
function rewriteFsHeapDirectViaCarrier(m: Module, layout: BucketLayout): Module {
|
|
1092
|
+
const fsDrawIdx = readScope("Input", HEAP_FS_DRAWIDX, Tu32);
|
|
1093
|
+
const fsInstId = readScope("Input", HEAP_FS_INSTID, Tu32);
|
|
1094
|
+
|
|
1095
|
+
// Declare the carrier(s) as FRAGMENT INPUTS. The decoder already writes them
|
|
1096
|
+
// on the vertex side; without the matching input declaration the emitter has
|
|
1097
|
+
// no struct member to resolve the read against (it reaches for the output
|
|
1098
|
+
// struct and the module fails to parse).
|
|
1099
|
+
const fsInputs: EntryParameter[] = [{
|
|
1100
|
+
name: HEAP_FS_DRAWIDX, type: Tu32, semantic: HEAP_FS_DRAWIDX,
|
|
1101
|
+
decorations: [{ kind: "Interpolation", mode: "flat" } as ParamDecoration],
|
|
1102
|
+
}];
|
|
1103
|
+
if (layout.perInstanceUniforms.size > 0) {
|
|
1104
|
+
fsInputs.push({
|
|
1105
|
+
name: HEAP_FS_INSTID, type: Tu32, semantic: HEAP_FS_INSTID,
|
|
1106
|
+
decorations: [{ kind: "Interpolation", mode: "flat" } as ParamDecoration],
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
let out = addFsInputs(m, fsInputs);
|
|
1110
|
+
|
|
1111
|
+
// Atlas samples first (they read header sub-fields, not uniform values).
|
|
1112
|
+
out = rewriteFsAtlasTexturesDirect(out, layout, fsDrawIdx);
|
|
1113
|
+
|
|
1114
|
+
// Which per-draw uniforms does the FS still read?
|
|
1115
|
+
const uniformFields = new Map<string, DrawHeaderField>();
|
|
1116
|
+
for (const f of layout.drawHeaderFields) {
|
|
1117
|
+
if (f.kind === "uniform-ref") uniformFields.set(f.name, f);
|
|
1118
|
+
}
|
|
1119
|
+
const used = new Set<string>();
|
|
1120
|
+
for (const v of out.values) {
|
|
1121
|
+
if (v.kind !== "Entry" || v.entry.stage !== "fragment") continue;
|
|
1122
|
+
for (const sn of readInputs(v.entry.body).values()) {
|
|
1123
|
+
if (sn.scope === "Input" && uniformFields.has(sn.name)) used.add(sn.name);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
if (used.size === 0) return out;
|
|
1127
|
+
|
|
1128
|
+
const subst = new Map<string, Expr>();
|
|
1129
|
+
for (const name of used) {
|
|
1130
|
+
const f = uniformFields.get(name)!;
|
|
1131
|
+
const refExpr = loadHeaderRef(fsDrawIdx, f.byteOffset, layout.strideU32);
|
|
1132
|
+
subst.set(name, isInlineHeaderField(f)
|
|
1133
|
+
? refExpr
|
|
1134
|
+
: layout.perInstanceUniforms.has(name)
|
|
1135
|
+
? loadInstanceByRef(refExpr, fsInstId, f.uniformWgslType ?? "")
|
|
1136
|
+
: loadUniformByRef(refExpr, f.uniformWgslType ?? ""));
|
|
1137
|
+
}
|
|
1138
|
+
return substituteInputsInStage(out, "fragment", "Input", n => subst.get(n));
|
|
1139
|
+
}
|
|
1140
|
+
|
|
977
1141
|
function compileHeapEffectIRViaDecoder(
|
|
978
1142
|
userEffect: Effect,
|
|
979
1143
|
layout: BucketLayout,
|
|
@@ -1020,9 +1184,14 @@ function compileHeapEffectIRViaDecoder(
|
|
|
1020
1184
|
combined = liftReturns(combined);
|
|
1021
1185
|
combined = injectVsBuiltins(combined);
|
|
1022
1186
|
|
|
1023
|
-
// 6. FS-side
|
|
1024
|
-
//
|
|
1025
|
-
|
|
1187
|
+
// 6. FS-side heap access. The decoder no longer ships per-draw VALUES across
|
|
1188
|
+
// the stage boundary (that cost one varying per uniform and four per texture,
|
|
1189
|
+
// against WebGPU's 16-location cap — see heapDecoder). It ships the draw
|
|
1190
|
+
// INDEX, and the FS loads what it needs straight from the arena. Whatever the
|
|
1191
|
+
// fragment stage stops reading, `pruneCrossStage` then drops from the carrier
|
|
1192
|
+
// by itself, so the varying count collapses to what genuinely varies per
|
|
1193
|
+
// vertex.
|
|
1194
|
+
combined = rewriteFsHeapDirectViaCarrier(combined, layout);
|
|
1026
1195
|
|
|
1027
1196
|
// 6b. USER storage buffers (the OIT node pool & co): pin each to the
|
|
1028
1197
|
// slot the bucket's BGL committed to. `keep: true` makes the emitter
|
package/src/runtime/heapScene.ts
CHANGED
|
@@ -790,7 +790,7 @@ export type HeapTextureSet =
|
|
|
790
790
|
* placement for `sourceAval` without holding a direct pool
|
|
791
791
|
* reference.
|
|
792
792
|
*/
|
|
793
|
-
readonly repack?: (newTex: ITexture) => import("./textureAtlas/atlasPool.js").AtlasAcquisition;
|
|
793
|
+
readonly repack?: (newTex: ITexture) => import("./textureAtlas/atlasPool.js").AtlasAcquisition | null;
|
|
794
794
|
/**
|
|
795
795
|
* Host source captured at adapter time, used by the heap-side
|
|
796
796
|
* re-acquire path when the pool entry was evicted while this spec
|
|
@@ -1397,7 +1397,7 @@ export function buildHeapScene(
|
|
|
1397
1397
|
interface AtlasAvalRef {
|
|
1398
1398
|
readonly bucket: Bucket;
|
|
1399
1399
|
readonly localSlot: number;
|
|
1400
|
-
readonly repack: (newTex: ITexture) => import("./textureAtlas/atlasPool.js").AtlasAcquisition;
|
|
1400
|
+
readonly repack: (newTex: ITexture) => import("./textureAtlas/atlasPool.js").AtlasAcquisition | null;
|
|
1401
1401
|
readonly sampler: ISampler;
|
|
1402
1402
|
}
|
|
1403
1403
|
// Content-keyed (`HashTable`, not a JS `Map`) so distinct
|
|
@@ -3684,6 +3684,15 @@ export function buildHeapScene(
|
|
|
3684
3684
|
...(host !== undefined ? { source: { width: reW, height: reH, host } } : {}),
|
|
3685
3685
|
},
|
|
3686
3686
|
);
|
|
3687
|
+
// ATLAS FULL (acq === null). Every page is packed with textures still
|
|
3688
|
+
// in use, so this one cannot be re-placed right now. Keep the spec's
|
|
3689
|
+
// existing sub-rect and retry on a later cycle, once a released tile
|
|
3690
|
+
// has freed space: the draw shows a stale texture for a frame or two
|
|
3691
|
+
// instead of the correct one. Previously `acquire` THREW here, the
|
|
3692
|
+
// exception escaped into the render loop's frame callback, and the
|
|
3693
|
+
// whole viewer stopped rendering permanently — taking the tile cut
|
|
3694
|
+
// with it, since the worker's camera is driven from `OnRendered`.
|
|
3695
|
+
if (acq !== null) {
|
|
3687
3696
|
(spec.textures as { pageId: number }).pageId = acq.pageId;
|
|
3688
3697
|
(spec.textures as { origin: V2f }).origin = acq.origin;
|
|
3689
3698
|
(spec.textures as { size: V2f }).size = acq.size;
|
|
@@ -3703,6 +3712,7 @@ export function buildHeapScene(
|
|
|
3703
3712
|
const retarget = (spec.textures as unknown as { __retarget?: (ref: number) => void }).__retarget;
|
|
3704
3713
|
if (retarget !== undefined) retarget(acq.ref);
|
|
3705
3714
|
packAtlasTextureFields(bucket, localSlot, spec.textures);
|
|
3715
|
+
}
|
|
3706
3716
|
}
|
|
3707
3717
|
}
|
|
3708
3718
|
bucket.localAtlasReleases[localSlot] = spec.textures.release;
|
|
@@ -4506,6 +4516,9 @@ export function buildHeapScene(
|
|
|
4506
4516
|
if (refs === undefined || refs.length === 0) continue;
|
|
4507
4517
|
const newTex = av.getValue(tok);
|
|
4508
4518
|
const acq = refs[0]!.repack(newTex);
|
|
4519
|
+
// Atlas full → the texture keeps its current sub-rect this cycle; the
|
|
4520
|
+
// draw shows the previous pixels rather than the pass dying.
|
|
4521
|
+
if (acq === null) continue;
|
|
4509
4522
|
for (const r of refs) {
|
|
4510
4523
|
const newTextures: HeapTextureSet & { kind: "atlas" } = {
|
|
4511
4524
|
kind: "atlas",
|
|
@@ -248,12 +248,20 @@ export function compileHybridScene(
|
|
|
248
248
|
// WeakMap keyed by RO ensures the same RO always maps to the same
|
|
249
249
|
// spec instance, even if the RO migrates out and back in.
|
|
250
250
|
const specCache = new WeakMap<RenderObject, HeapDrawSpec>();
|
|
251
|
-
|
|
251
|
+
// A null spec means "cannot be placed right now" — today: the texture atlas is
|
|
252
|
+
// full. Such an RO is dropped from this cycle's heap set instead of rendering,
|
|
253
|
+
// and NOT cached, so the next cycle retries it (by then a released tile will
|
|
254
|
+
// usually have freed atlas space). It costs a missing tile for a frame or two;
|
|
255
|
+
// the alternative was an exception in the frame callback, which killed the
|
|
256
|
+
// render loop outright.
|
|
257
|
+
const heapSpecAset = heapAset.choose((ro: RenderObject) => {
|
|
252
258
|
let spec = specCache.get(ro);
|
|
253
259
|
if (spec === undefined) {
|
|
254
|
-
|
|
260
|
+
const built = renderObjectToHeapSpec(ro, AdaptiveToken.top, atlasPool, {
|
|
255
261
|
enableDerivedUniforms: opts.enableDerivedUniforms !== false,
|
|
256
262
|
});
|
|
263
|
+
if (built === null) return undefined;
|
|
264
|
+
spec = built;
|
|
257
265
|
specCache.set(ro, spec);
|
|
258
266
|
}
|
|
259
267
|
return spec;
|
|
@@ -275,6 +275,15 @@ export class AtlasPool {
|
|
|
275
275
|
* toggle. With the LRU it fires only on first acquire.
|
|
276
276
|
*/
|
|
277
277
|
private readonly lru = new Map<number, AtlasEntry>();
|
|
278
|
+
/** Formats we've already reported as full (once per format, not per frame). */
|
|
279
|
+
private readonly fullWarned = new Set<AtlasPageFormat>();
|
|
280
|
+
/** True while every page of `format` is packed with live (referenced) entries. */
|
|
281
|
+
isFull(format: AtlasPageFormat): boolean {
|
|
282
|
+
const pages = this.pagesByFormat.get(format);
|
|
283
|
+
if (pages === undefined || pages.length < ATLAS_MAX_PAGES_PER_FORMAT) return false;
|
|
284
|
+
for (const e of this.lru.values()) if (e.format === format) return false; // something is evictable
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
278
287
|
private nextRef = 1;
|
|
279
288
|
|
|
280
289
|
private innerKeyOf(t: ITexture): GPUTexture | HostTextureSource {
|
|
@@ -378,7 +387,9 @@ export class AtlasPool {
|
|
|
378
387
|
width: number,
|
|
379
388
|
height: number,
|
|
380
389
|
opts: AtlasAcquireOptions = {},
|
|
381
|
-
|
|
390
|
+
// Returns null when the atlas is FULL (every page packed with live
|
|
391
|
+
// entries). Callers must handle it — see the note at the throw site.
|
|
392
|
+
): AtlasAcquisition | null {
|
|
382
393
|
const existing = this.entriesByAval.get(sourceAval);
|
|
383
394
|
if (existing !== undefined) {
|
|
384
395
|
const wasIdle = existing.refcount === 0;
|
|
@@ -485,9 +496,27 @@ export class AtlasPool {
|
|
|
485
496
|
|
|
486
497
|
// Allocate a fresh page if we haven't hit the max.
|
|
487
498
|
if (pages.length >= ATLAS_MAX_PAGES_PER_FORMAT) {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
499
|
+
// The atlas is genuinely full: every page is packed with entries that are
|
|
500
|
+
// still referenced, so there is nothing to evict. That is resource
|
|
501
|
+
// pressure, NOT a program error — and throwing turned it into a fatal
|
|
502
|
+
// one: the exception escapes into the render loop's frame callback, which
|
|
503
|
+
// then dies (permanently, before `runFrame` learned to retry; and even
|
|
504
|
+
// with retries the condition repeats every frame until the loop gives up).
|
|
505
|
+
// A viewer that streams an unbounded world will hit this, and the right
|
|
506
|
+
// answer is to skip the texture for now, not to stop rendering.
|
|
507
|
+
//
|
|
508
|
+
// The caller decides what "skip" means (see heapScene: the draw is held
|
|
509
|
+
// back until space frees). We report it once per format so the condition
|
|
510
|
+
// is visible without spamming a frame-rate-long log.
|
|
511
|
+
if (!this.fullWarned.has(format)) {
|
|
512
|
+
this.fullWarned.add(format);
|
|
513
|
+
console.warn(
|
|
514
|
+
`atlas: FULL for ${format} — ${pages.length} pages × ${ATLAS_PAGE_SIZE}² all in use ` +
|
|
515
|
+
`(${this.entriesByRef.size} live entries, ${this.lru.size} evictable). ` +
|
|
516
|
+
`New textures are deferred until space frees.`,
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
return null;
|
|
491
520
|
}
|
|
492
521
|
const pageId = pages.length;
|
|
493
522
|
const page = this.allocatePage(format, pageId);
|
|
@@ -531,7 +560,8 @@ export class AtlasPool {
|
|
|
531
560
|
sourceAval: aval<ITexture>,
|
|
532
561
|
newTexture: ITexture,
|
|
533
562
|
opts: AtlasAcquireOptions = {},
|
|
534
|
-
|
|
563
|
+
// null when the atlas is full — caller keeps its existing sub-rect.
|
|
564
|
+
): AtlasAcquisition | null {
|
|
535
565
|
const old = this.entriesByAval.get(sourceAval);
|
|
536
566
|
if (old === undefined) {
|
|
537
567
|
throw new Error("AtlasPool.repack: source aval has no live entry");
|
|
@@ -587,6 +617,10 @@ export class AtlasPool {
|
|
|
587
617
|
: {}),
|
|
588
618
|
};
|
|
589
619
|
const acq = this.acquire(fmt, sourceAval, desc.width, desc.height, acqOpts);
|
|
620
|
+
// Atlas full: the caller keeps the OLD sub-rect (already freed above is not
|
|
621
|
+
// possible here — repack only frees after a successful acquire), so the
|
|
622
|
+
// texture simply doesn't update this cycle.
|
|
623
|
+
if (acq === null) return null;
|
|
590
624
|
|
|
591
625
|
// 3. Restore the original refcount (acquire set it to 1).
|
|
592
626
|
const newEntry = this.entriesByRef.get(acq.ref);
|