@woosh/meep-engine 3.63.0 → 3.63.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/CHANGELOG.md CHANGED
@@ -12,11 +12,14 @@
12
12
  `ShadedGeometryOutlineSystem` pair `Outline` with models and primitives, and `OutlineSerializationAdapter` is
13
13
  registered. Each pixel belongs to one spec: the front surface's visible spec, otherwise the hidden spec of the
14
14
  nearest row with one behind it. Outlines come from a jump flood that keeps each pixel's two nearest specs, run
15
- only on the screen tiles an outline can reach, and the nearer outline is drawn on top. A spec shared by several
15
+ only on the screen tiles within reach of a silhouette, and the nearer outline is drawn on top; the inside of a
16
+ large object is drawn as its fill alone, and not at all when the fill is transparent. A spec shared by several
16
17
  entities outlines them as one shape. Playground: `src/shade/playground/outline/`.
17
18
  - **Changed:** outlines of hidden parts are back, for an entity whose `Outline` has a hidden spec. At 1898x982
18
- with a 14 px reach and four outlined entities, one with a hidden spec, the passes cost 0.44 ms of frame time on
19
- an RTX 4090 and 4.4 ms on an AMD RDNA2 iGPU (flood 2.5, classify 1.1, composite 0.16, hidden-row raster 0.13).
19
+ with a 14 px reach and four outlined entities, one with a hidden spec, the passes cost 0.26 ms of frame time on
20
+ an RTX 4090 and 2.9 ms on an AMD RDNA2 iGPU (flood 1.6, classify 0.7, composite and fill 0.15). With the
21
+ removed highlight's look, a 3 px line around one object, they cost 0.6–1.3 ms of GPU time on that iGPU from an
22
+ object 50 px tall to one filling the screen, where the highlight pass cost 10.1–6.9 ms.
20
23
  - **Added:** the scene database's `mesh_outlines` table; `GPUViewContext.follow(view)` and
21
24
  `GPUViewContext.upscale_ratio`; `GPUSceneContext.graphics`; a `visible_meshlets` argument and result on
22
25
  `graph_rasterize_scene_fast_raw`; `ScreenTileShader` and `chunk_screen_tile_corner`
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "description": "Advanced 3D game engine",
7
7
  "type": "module",
8
8
  "author": "Alexander Goldring",
9
- "version": "3.63.0",
9
+ "version": "3.63.1",
10
10
  "exports": {
11
11
  "./build/*": "./build/*",
12
12
  "./src/*": "./src/*",
@@ -39,9 +39,12 @@ import { OUTLINE_TILE_SIZE } from "./OUTLINE_TILE_SIZE.js";
39
39
  import { shader_outline_classify } from "./shader_outline_classify.js";
40
40
  import { shader_outline_classify_hidden } from "./shader_outline_classify_hidden.js";
41
41
  import { shader_outline_composite } from "./shader_outline_composite.js";
42
+ import { shader_outline_fill } from "./shader_outline_fill.js";
43
+ import { shader_outline_fill_tiles } from "./shader_outline_fill_tiles.js";
42
44
  import { shader_outline_flood_seed } from "./shader_outline_flood_seed.js";
43
45
  import { shader_outline_flood_step } from "./shader_outline_flood_step.js";
44
46
  import { OUTLINE_ROW_CULL_GROUP_SIZE, shader_outline_row_cull } from "./shader_outline_row_cull.js";
47
+ import { OUTLINE_TILE_GRID_GROUP_SIZE, shader_outline_tile_edges } from "./shader_outline_tile_edges.js";
45
48
 
46
49
  /**
47
50
  * Words a `COLLECTION_U32` keeps ahead of its elements: the count, padded to the elements' 16-byte
@@ -69,11 +72,13 @@ const TILE_DRAW_COMMAND_PATTERN = "6u, input, 0u, 0u";
69
72
  /**
70
73
  * Records the outline passes into a frame.
71
74
  *
72
- * In order: the outline raster of the rows with a hidden spec (only when there are any), the
73
- * full-screen classify that labels every pixel with the spec that owns it, the tile dilation that
74
- * finds the tiles an outline can reach and lists them, the jump-flood rounds over those tiles, and a
75
- * composite of tile quads blended into the scene colour. Every pass after the classify is sized by the
76
- * live tile count, read on the device.
75
+ * In order: the outline raster of the rows with a hidden spec (only when there are any); the
76
+ * full-screen classify that labels every pixel with the spec that owns it and every tile with the
77
+ * label its pixels share; the tiles a silhouette passes through, dilated by the outline's reach and
78
+ * listed — the flood's tiles; the list of the other tiles that show a fill alone; the jump-flood rounds
79
+ * over the flood's tiles; and tile quads blended into the scene colour, a composite over the flood's
80
+ * tiles and a fill over the rest. The flood follows the silhouettes' length, not the area they enclose.
81
+ * Every pass after the tile passes is sized by a live tile count, read on the device.
77
82
  */
78
83
  export class GPUOutlineRenderer {
79
84
  /**
@@ -186,7 +191,7 @@ export class GPUOutlineRenderer {
186
191
  outline_visibility: back.visibility,
187
192
  outline_meshlets: back.visible_meshlets,
188
193
  labels: label_descriptor(resolution),
189
- tile_seeds: label_descriptor(tile_resolution),
194
+ tile_labels: label_descriptor(tile_resolution),
190
195
  }
191
196
  });
192
197
  } else {
@@ -200,18 +205,38 @@ export class GPUOutlineRenderer {
200
205
  visible_meshlets: view_textures.visible_meshlets,
201
206
  scene_database: scene.scene_database_buffer,
202
207
  labels: label_descriptor(resolution),
203
- tile_seeds: label_descriptor(tile_resolution),
208
+ tile_labels: label_descriptor(tile_resolution),
204
209
  }
205
210
  });
206
211
  }
207
212
 
208
- const { labels, tile_seeds } = classify;
213
+ const { labels, tile_labels } = classify;
209
214
 
210
- const { tile_active, tiles } = this.#graph_reach({ graph, tile_seeds, tile_resolution, steps: plan.reach_steps });
215
+ const tile_grid_group_count = [
216
+ Math.ceil(tile_resolution[0] / OUTLINE_TILE_GRID_GROUP_SIZE),
217
+ Math.ceil(tile_resolution[1] / OUTLINE_TILE_GRID_GROUP_SIZE),
218
+ 1
219
+ ];
220
+
221
+ const edges = graph_compute_pass({
222
+ graph,
223
+ shader: shader_outline_tile_edges,
224
+ group_count: tile_grid_group_count,
225
+ bindings: {
226
+ settings: { resolution: tile_resolution },
227
+ tile_labels,
228
+ edges: label_descriptor(tile_resolution),
229
+ }
230
+ }).edges;
231
+
232
+ const { tile_active, tiles } = this.#graph_reach({ graph, edges, tile_resolution, steps: plan.reach_steps });
233
+
234
+ const fill_tiles = this.#graph_fill_tiles({ graph, specs, tile_labels, tile_active, tile_resolution, group_count: tile_grid_group_count });
211
235
 
212
236
  const dispatch = graph_build_indirect_dispatch_command(graph, tiles, 1, true);
213
237
 
214
- const draw = this.#graph_tile_draw_command(graph, tiles);
238
+ const draw = this.#graph_tile_draw_command(graph, tiles, 'outline / tile draw command');
239
+ const fill_draw = this.#graph_tile_draw_command(graph, fill_tiles, 'outline / fill draw command');
215
240
 
216
241
  let state = graph_compute_indirect_pass({
217
242
  graph,
@@ -220,7 +245,7 @@ export class GPUOutlineRenderer {
220
245
  bindings: {
221
246
  settings: { resolution, step: plan.flood_steps[0] },
222
247
  labels,
223
- tile_seeds,
248
+ tile_labels,
224
249
  tiles,
225
250
  state_out: state_descriptor(resolution),
226
251
  }
@@ -242,7 +267,8 @@ export class GPUOutlineRenderer {
242
267
  }).state_out;
243
268
  }
244
269
 
245
- this.#graph_composite({ frame, resolution, jitter, upscale, specs, tiles, draw, state, labels, tile_seeds });
270
+ this.#graph_composite({ frame, resolution, jitter, upscale, specs, tiles, draw, state, labels, tile_labels });
271
+ this.#graph_fill({ frame, resolution, jitter, upscale, specs, fill_tiles, fill_draw, tile_labels });
246
272
  }
247
273
 
248
274
  /**
@@ -339,18 +365,19 @@ export class GPUOutlineRenderer {
339
365
  }
340
366
 
341
367
  /**
342
- * Dilate the seed tiles by the outline's reach, and list the tiles that come out set.
368
+ * Dilate the edge tiles by the outline's reach, and list the tiles that come out set: the flood's
369
+ * tiles.
343
370
  *
344
371
  * @returns {{tile_active: number, tiles: number}}
345
372
  */
346
- #graph_reach({ graph, tile_seeds, tile_resolution, steps }) {
373
+ #graph_reach({ graph, edges, tile_resolution, steps }) {
347
374
  const group_count = [
348
375
  Math.ceil(tile_resolution[0] / JUMP_DILATE_GROUP_SIZE),
349
376
  Math.ceil(tile_resolution[1] / JUMP_DILATE_GROUP_SIZE),
350
377
  1
351
378
  ];
352
379
 
353
- let mask = tile_seeds;
380
+ let mask = edges;
354
381
 
355
382
  for (let i = 0; i < steps.length - 1; i++) {
356
383
  mask = graph_compute_pass({
@@ -384,15 +411,53 @@ export class GPUOutlineRenderer {
384
411
  return { tile_active: last.mask_out, tiles: last.list };
385
412
  }
386
413
 
414
+ /**
415
+ * List the tiles outside the flood that show a fill alone.
416
+ *
417
+ * @returns {number} the list, a `COLLECTION_U32`
418
+ */
419
+ #graph_fill_tiles({ graph, specs, tile_labels, tile_active, tile_resolution, group_count }) {
420
+ const data = {};
421
+
422
+ const pass = graph.add(shader_outline_fill_tiles.descriptor.label, data, (data, resources, context) => {
423
+ shader_outline_fill_tiles.dispatch({
424
+ encoder: context.encoder,
425
+ group_count_x: group_count[0],
426
+ group_count_y: group_count[1],
427
+ group_count_z: group_count[2],
428
+ bindings: {
429
+ settings: { resolution: tile_resolution },
430
+ tile_labels: resources.get(data.tile_labels).obtainView(),
431
+ tile_active: resources.get(data.tile_active).obtainView(),
432
+ // read when the pass runs: the table replaces its buffer when it grows
433
+ specs: specs.buffer,
434
+ fill_tiles: resources.get(data.fill_tiles),
435
+ }
436
+ });
437
+ });
438
+
439
+ data.tile_labels = pass.read(tile_labels);
440
+ data.tile_active = pass.read(tile_active);
441
+
442
+ data.fill_tiles = pass.create('outline / fill tiles', BufferResourceDescriptor.from(
443
+ (COLLECTION_HEADER_WORDS + tile_resolution[0] * tile_resolution[1]) * 4,
444
+ GPUBufferUsage.STORAGE,
445
+ 0, 16 // clear the count
446
+ ));
447
+
448
+ return data.fill_tiles;
449
+ }
450
+
387
451
  /**
388
452
  * @param {FrameGraph} graph
389
453
  * @param {number} tiles
454
+ * @param {string} label
390
455
  * @returns {number} the tile quads' draw command
391
456
  */
392
- #graph_tile_draw_command(graph, tiles) {
457
+ #graph_tile_draw_command(graph, tiles, label) {
393
458
  const data = {};
394
459
 
395
- const pass = graph.add('outline / tile draw command', data, (data, resources, context) => {
460
+ const pass = graph.add(label, data, (data, resources, context) => {
396
461
  const encoder = context.encoder;
397
462
 
398
463
  build_indirect_draw_command(encoder.device, TILE_DRAW_COMMAND_PATTERN, encoder, resources.get(data.tiles), resources.get(data.command));
@@ -401,7 +466,7 @@ export class GPUOutlineRenderer {
401
466
  data.tiles = pass.read(tiles);
402
467
 
403
468
  // a buffer of its own: an indirect argument may not be bound writable in the scope that uses it
404
- data.command = pass.create('outline / tile draw command', BufferResourceDescriptor.from(
469
+ data.command = pass.create(label, BufferResourceDescriptor.from(
405
470
  STRUCT_UNINDEXED_DRAW_COMMAND.aligned_size,
406
471
  GPUBufferUsage.STORAGE | GPUBufferUsage.INDIRECT,
407
472
  ));
@@ -409,7 +474,7 @@ export class GPUOutlineRenderer {
409
474
  return data.command;
410
475
  }
411
476
 
412
- #graph_composite({ frame, resolution, jitter, upscale, specs, tiles, draw, state, labels, tile_seeds }) {
477
+ #graph_composite({ frame, resolution, jitter, upscale, specs, tiles, draw, state, labels, tile_labels }) {
413
478
  const graph = frame.graph;
414
479
  const scene = frame.get(SceneColor);
415
480
 
@@ -425,7 +490,7 @@ export class GPUOutlineRenderer {
425
490
  tiles: resources.get(data.tiles),
426
491
  state: resources.get(data.state).obtainView(),
427
492
  labels: resources.get(data.labels).obtainView(),
428
- tile_seeds: resources.get(data.tile_seeds).obtainView(),
493
+ tile_labels: resources.get(data.tile_labels).obtainView(),
429
494
  // read when the pass runs: the table replaces its buffer when it grows
430
495
  specs: specs.buffer,
431
496
  mask_atlas: this.#atlas.obtainView(),
@@ -444,7 +509,7 @@ export class GPUOutlineRenderer {
444
509
  data.draw = pass.read(draw);
445
510
  data.state = pass.read(state);
446
511
  data.labels = pass.read(labels);
447
- data.tile_seeds = pass.read(tile_seeds);
512
+ data.tile_labels = pass.read(tile_labels);
448
513
 
449
514
  // read and written, because the outline blends over what is already there
450
515
  data.color = pass.write(pass.read(scene.color));
@@ -452,6 +517,44 @@ export class GPUOutlineRenderer {
452
517
  scene.color = data.color;
453
518
  }
454
519
 
520
+ #graph_fill({ frame, resolution, jitter, upscale, specs, fill_tiles, fill_draw, tile_labels }) {
521
+ const graph = frame.graph;
522
+ const scene = frame.get(SceneColor);
523
+
524
+ const data = {};
525
+
526
+ const pass = graph.add('outline / fill', data, (data, resources, context) => {
527
+ const color = resources.get(data.color);
528
+
529
+ shader_outline_fill.draw({
530
+ encoder: context.encoder,
531
+ bindings: {
532
+ settings: { resolution, jitter, upscale },
533
+ tiles: resources.get(data.tiles),
534
+ tile_labels: resources.get(data.tile_labels).obtainView(),
535
+ // read when the pass runs: the table replaces its buffer when it grows
536
+ specs: specs.buffer,
537
+ mask_atlas: this.#atlas.obtainView(),
538
+ },
539
+ colorAttachments: [{
540
+ loadOp: "load",
541
+ storeOp: "store",
542
+ view: color.obtainView()
543
+ }],
544
+ color_formats: [color.descriptor.format],
545
+ command: resources.get(data.draw),
546
+ });
547
+ });
548
+
549
+ data.tiles = pass.read(fill_tiles);
550
+ data.draw = pass.read(fill_draw);
551
+ data.tile_labels = pass.read(tile_labels);
552
+
553
+ data.color = pass.write(pass.read(scene.color));
554
+
555
+ scene.color = data.color;
556
+ }
557
+
455
558
  /**
456
559
  * @param {GraphicsContext} graphics
457
560
  * @param {GPUViewContext} view
@@ -0,0 +1,77 @@
1
+ import { WebGPUExtensionType } from "../../../shade/descriptor/WebGPUExtensionType.js";
2
+ import { CodeChunk } from "../../../shade/renderer/shader/compiler/CodeChunk.js";
3
+ import { chunk_outline_label_words } from "./chunk_outline_label_words.js";
4
+
5
+ /**
6
+ * The end of a classify pass: every lane of a tile's workgroup hands in its pixel's label, and the
7
+ * tile's label word and its labels are stored.
8
+ *
9
+ * - The tile's word, in `tile_labels`, is the label all its on-screen pixels hold, `OUTLINE_NONE`
10
+ * when that is no label, and `OUTLINE_TILE_MIXED` when they differ. A lane past the screen's edge
11
+ * takes no part.
12
+ * - The pixel's label, in `labels`, is stored only in a tile whose word is not `OUTLINE_NONE`, so
13
+ * `labels` is valid in those tiles and nowhere else: a reader checks the tile's word first.
14
+ *
15
+ * The word comes from the maximum and the minimum label over the tile, the minimum kept as the
16
+ * maximum of the complements so that zeroed workgroup memory is the identity of both. Each subgroup
17
+ * reduces first, so the workgroup's atomics are one per subgroup, not one per lane.
18
+ *
19
+ * Declares the workgroup variables it reduces through; reads `tile_labels` and `labels`, which every
20
+ * pass using it declares. Call from uniform control flow, by every lane.
21
+ *
22
+ * @type {CodeChunk}
23
+ */
24
+ export const chunk_outline_classify_store = CodeChunk.from(
25
+ //language=WGSL
26
+ `
27
+ var<workgroup> outline_tile_highest: atomic<u32>;
28
+ var<workgroup> outline_tile_lowest_complement: atomic<u32>;
29
+ var<workgroup> outline_tile_word: u32;
30
+
31
+ fn outline_classify_store(tile: vec2<u32>, pixel: vec2<u32>, lane: u32, inside: bool, label: u32) {
32
+ // 0 is the identity of a maximum, so a lane off the screen contributes nothing
33
+ var highest = 0u;
34
+ var lowest_complement = 0u;
35
+
36
+ if (inside) {
37
+ highest = label;
38
+ lowest_complement = ~label;
39
+ }
40
+
41
+ let subgroup_highest = subgroupMax(highest);
42
+ let subgroup_lowest_complement = subgroupMax(lowest_complement);
43
+
44
+ if (subgroupElect()) {
45
+ atomicMax(&outline_tile_highest, subgroup_highest);
46
+ atomicMax(&outline_tile_lowest_complement, subgroup_lowest_complement);
47
+ }
48
+
49
+ workgroupBarrier();
50
+
51
+ // one lane reads the reduction and hands the word on: an atomic load is a read-modify-write on
52
+ // some backends, and every lane loading the same word serialises on it
53
+ if (lane == 0u) {
54
+ let tile_highest = atomicLoad(&outline_tile_highest);
55
+ let tile_lowest = ~atomicLoad(&outline_tile_lowest_complement);
56
+
57
+ var tile_word = OUTLINE_TILE_MIXED;
58
+
59
+ if (tile_highest == tile_lowest) {
60
+ tile_word = tile_highest;
61
+ }
62
+
63
+ outline_tile_word = tile_word;
64
+
65
+ textureStore(tile_labels, vec2<i32>(tile), vec4<u32>(tile_word, 0u, 0u, 0u));
66
+ }
67
+
68
+ let word = workgroupUniformLoad(&outline_tile_word);
69
+
70
+ if (word != OUTLINE_NONE && inside) {
71
+ textureStore(labels, vec2<i32>(pixel), vec4<u32>(label, 0u, 0u, 0u));
72
+ }
73
+ }
74
+ `, [chunk_outline_label_words]
75
+ );
76
+
77
+ chunk_outline_classify_store.addExtension(WebGPUExtensionType.Subgroups);
@@ -5,19 +5,13 @@ import {
5
5
  } from "../../../shade/renderer/scene/MESH_OUTLINE_STRUCT.js";
6
6
  import { SCENE_DATABASE_SPEC } from "../../../shade/renderer/scene/SCENE_DATABASE_SPEC.js";
7
7
  import { CodeChunk } from "../../../shade/renderer/shader/compiler/CodeChunk.js";
8
+ import { chunk_outline_label_words } from "./chunk_outline_label_words.js";
8
9
 
9
10
  /**
10
11
  * The scene database's `mesh_outlines` table.
11
12
  */
12
13
  export const MESH_OUTLINES_TABLE = SCENE_DATABASE_SPEC.get('mesh_outlines');
13
14
 
14
- /**
15
- * The word for "no label" and for "no row".
16
- *
17
- * @type {number}
18
- */
19
- export const OUTLINE_NONE = 0xFFFFFFFF;
20
-
21
15
  /**
22
16
  * Which spec owns a pixel, if any.
23
17
  *
@@ -40,7 +34,6 @@ export const OUTLINE_NONE = 0xFFFFFFFF;
40
34
  export const chunk_outline_label = CodeChunk.from(
41
35
  //language=WGSL
42
36
  `
43
- const OUTLINE_NONE: u32 = ${OUTLINE_NONE}u;
44
37
  const MESH_OUTLINE_FLAG_VISIBLE: u32 = ${MESH_OUTLINE_FLAG_VISIBLE}u;
45
38
  const MESH_OUTLINE_FLAG_HIDDEN: u32 = ${MESH_OUTLINE_FLAG_HIDDEN}u;
46
39
 
@@ -81,5 +74,5 @@ fn outline_record_of_row(database: ptr<storage, array<u32>>, row: u32) -> ${MESH
81
74
 
82
75
  return ${MESH_OUTLINES_TABLE.marshalling_method_read}(database, row);
83
76
  }
84
- `, [MESH_OUTLINES_TABLE.chunk_read]
77
+ `, [chunk_outline_label_words, MESH_OUTLINES_TABLE.chunk_read]
85
78
  );
@@ -0,0 +1,29 @@
1
+ import { CodeChunk } from "../../../shade/renderer/shader/compiler/CodeChunk.js";
2
+
3
+ /**
4
+ * The word for "no label" and for "no row".
5
+ *
6
+ * @type {number}
7
+ */
8
+ export const OUTLINE_NONE = 0xFFFFFFFF;
9
+
10
+ /**
11
+ * The label word of a tile whose pixels do not all hold the same label, "no label" counting as one.
12
+ * Any other tile's word is the label all its pixels hold, or {@link OUTLINE_NONE}. Spec indices stay
13
+ * below both reserved words.
14
+ *
15
+ * @type {number}
16
+ */
17
+ export const OUTLINE_TILE_MIXED = 0xFFFFFFFE;
18
+
19
+ /**
20
+ * The reserved label words, for shaders.
21
+ *
22
+ * @type {CodeChunk}
23
+ */
24
+ export const chunk_outline_label_words = CodeChunk.from(
25
+ //language=WGSL
26
+ `
27
+ const OUTLINE_NONE: u32 = ${OUTLINE_NONE}u;
28
+ const OUTLINE_TILE_MIXED: u32 = ${OUTLINE_TILE_MIXED}u;
29
+ `);
@@ -9,6 +9,7 @@ import {
9
9
  VISIBLE_MESHLETS_RESOURCE
10
10
  } from "../../../shade/renderer/rasterize/visibility/chunk_visibility_decode.js";
11
11
  import { MESH_OUTLINE_STRUCT } from "../../../shade/renderer/scene/MESH_OUTLINE_STRUCT.js";
12
+ import { chunk_outline_classify_store } from "./chunk_outline_classify_store.js";
12
13
  import { chunk_outline_label } from "./chunk_outline_label.js";
13
14
  import { OUTLINE_TILE_SIZE } from "./OUTLINE_TILE_SIZE.js";
14
15
 
@@ -27,15 +28,14 @@ resources.createGroup()
27
28
 
28
29
  resources.createGroup()
29
30
  .addStorageTexture("labels", "r32uint", "2d", "write-only")
30
- .addStorageTexture("tile_seeds", "r32uint", "2d", "write-only");
31
+ .addStorageTexture("tile_labels", "r32uint", "2d", "write-only");
31
32
 
32
33
  /**
33
34
  * Labels every pixel with the spec that owns it, for a frame with no hidden specs: only the front
34
35
  * surface's visible spec can own a pixel. See {@link chunk_outline_label}.
35
36
  *
36
- * One workgroup per screen tile, over the whole screen. Every tile writes whether it holds a label;
37
- * only a tile that does writes its labels, so the label texture is valid inside those tiles and
38
- * nowhere else — a reader checks the tile first.
37
+ * One workgroup per screen tile, over the whole screen. Every tile writes its label word, and only a
38
+ * tile with a label writes its labels; see {@link chunk_outline_classify_store}.
39
39
  *
40
40
  * {@link shader_outline_classify_hidden} is the same pass for a frame that also drew the outline
41
41
  * raster.
@@ -48,8 +48,6 @@ export const shader_outline_classify = ComputeShader.from({
48
48
  body: CodeChunk.from(
49
49
  //language=WGSL
50
50
  `
51
- var<workgroup> tile_labelled: atomic<u32>;
52
-
53
51
  @compute @workgroup_size(${OUTLINE_TILE_SIZE}, ${OUTLINE_TILE_SIZE}, 1)
54
52
  fn main(
55
53
  @builtin(workgroup_id) tile: vec3<u32>,
@@ -76,23 +74,8 @@ fn main(
76
74
  label = outline_label(front_row, outline_record_of_row(&scene_database, front_row), OUTLINE_NONE, nothing_behind);
77
75
  }
78
76
 
79
- // workgroup memory starts zeroed
80
- if (label != OUTLINE_NONE) {
81
- atomicOr(&tile_labelled, 1u);
82
- }
83
-
84
- workgroupBarrier();
85
-
86
- let labelled = atomicLoad(&tile_labelled);
87
-
88
- if (lane == 0u) {
89
- textureStore(tile_seeds, vec2<i32>(tile.xy), vec4<u32>(labelled, 0u, 0u, 0u));
90
- }
91
-
92
- if (labelled != 0u && inside) {
93
- textureStore(labels, vec2<i32>(pixel), vec4<u32>(label, 0u, 0u, 0u));
94
- }
77
+ outline_classify_store(tile.xy, pixel, lane, inside, label);
95
78
  }
96
- `, [chunk_visibility_decode, chunk_outline_label]
79
+ `, [chunk_visibility_decode, chunk_outline_label, chunk_outline_classify_store]
97
80
  )
98
81
  });
@@ -8,6 +8,7 @@ import {
8
8
  } from "../../../shade/renderer/rasterize/visibility/chunk_visibility_decode.js";
9
9
  import { MESHLET_COLLECTION_STRUCT } from "../../../shade/renderer/rasterize/MESHLET_COLLECTION_STRUCT.js";
10
10
  import { VISIBILITY_TRIANGLE_BITS } from "../../../shade/renderer/rasterize/visibility/VISIBILITY_BUFFER.js";
11
+ import { chunk_outline_classify_store } from "./chunk_outline_classify_store.js";
11
12
  import { chunk_outline_label } from "./chunk_outline_label.js";
12
13
  import { OUTLINE_TILE_SIZE } from "./OUTLINE_TILE_SIZE.js";
13
14
  import { OUTLINE_CLASSIFY_SETTINGS } from "./shader_outline_classify.js";
@@ -27,7 +28,7 @@ resources.createGroup()
27
28
 
28
29
  resources.createGroup()
29
30
  .addStorageTexture("labels", "r32uint", "2d", "write-only")
30
- .addStorageTexture("tile_seeds", "r32uint", "2d", "write-only");
31
+ .addStorageTexture("tile_labels", "r32uint", "2d", "write-only");
31
32
 
32
33
  /**
33
34
  * {@link shader_outline_classify} for a frame that drew the outline raster: a pixel the front surface
@@ -45,8 +46,6 @@ export const shader_outline_classify_hidden = ComputeShader.from({
45
46
  body: CodeChunk.from(
46
47
  //language=WGSL
47
48
  `
48
- var<workgroup> tile_labelled: atomic<u32>;
49
-
50
49
  @compute @workgroup_size(${OUTLINE_TILE_SIZE}, ${OUTLINE_TILE_SIZE}, 1)
51
50
  fn main(
52
51
  @builtin(workgroup_id) tile: vec3<u32>,
@@ -86,23 +85,8 @@ fn main(
86
85
  );
87
86
  }
88
87
 
89
- // workgroup memory starts zeroed
90
- if (label != OUTLINE_NONE) {
91
- atomicOr(&tile_labelled, 1u);
92
- }
93
-
94
- workgroupBarrier();
95
-
96
- let labelled = atomicLoad(&tile_labelled);
97
-
98
- if (lane == 0u) {
99
- textureStore(tile_seeds, vec2<i32>(tile.xy), vec4<u32>(labelled, 0u, 0u, 0u));
100
- }
101
-
102
- if (labelled != 0u && inside) {
103
- textureStore(labels, vec2<i32>(pixel), vec4<u32>(label, 0u, 0u, 0u));
104
- }
88
+ outline_classify_store(tile.xy, pixel, lane, inside, label);
105
89
  }
106
- `, [chunk_visibility_decode, chunk_outline_label]
90
+ `, [chunk_visibility_decode, chunk_outline_label, chunk_outline_classify_store]
107
91
  )
108
92
  });
@@ -9,6 +9,7 @@ import { ScreenTileShader } from "../../../shade/renderer/shader/tile/ScreenTile
9
9
  import { vec2f, vec2u } from "../../../shade/renderer/shader/type/WEBGPU_WGSL_PRIMITIVE_TYPES.js";
10
10
  import { WebGPUArray } from "../../../shade/renderer/shader/type/WebGPUArray.js";
11
11
  import { WebGPUStruct } from "../../../shade/renderer/shader/type/WebGPUStruct.js";
12
+ import { chunk_outline_label_words } from "./chunk_outline_label_words.js";
12
13
  import { chunk_outline_profile } from "./chunk_outline_profile.js";
13
14
  import { OUTLINE_SPEC_STRUCT } from "./OUTLINE_SPEC_STRUCT.js";
14
15
  import { OUTLINE_TILE_SIZE } from "./OUTLINE_TILE_SIZE.js";
@@ -33,13 +34,13 @@ resources.createGroup()
33
34
  .addStorageBuffer("tiles", COLLECTION_U32_STRUCT_READ)
34
35
  .addTexture("state", "uint")
35
36
  .addTexture("labels", "uint")
36
- .addTexture("tile_seeds", "uint")
37
+ .addTexture("tile_labels", "uint")
37
38
  .addStorageBuffer("specs", WebGPUArray.from(OUTLINE_SPEC_STRUCT))
38
39
  .addTexture("mask_atlas", "unfilterable-float");
39
40
 
40
41
  /**
41
- * Places instance `i` on the `i`-th active tile. Outline tiles count rows down from the top, as
42
- * pixels do, so clip-space y is flipped.
42
+ * Places instance `i` on the `i`-th listed tile. Outline tiles count rows down from the top, as
43
+ * pixels do, so clip-space y is flipped. Reads `tiles` and `settings.resolution`.
43
44
  *
44
45
  * @type {CodeChunk}
45
46
  */
@@ -60,7 +61,13 @@ fn main(
60
61
  `, [chunk_screen_tile_corner]
61
62
  );
62
63
 
63
- const body = CodeChunk.from(
64
+ /**
65
+ * `outline_mask`: the factor a spec's mask multiplies its part by at an output pixel, read from
66
+ * `mask_atlas`, which every pass using it declares. See {@link chunk_outline_profile}.
67
+ *
68
+ * @type {CodeChunk}
69
+ */
70
+ export const chunk_outline_mask = CodeChunk.from(
64
71
  //language=WGSL
65
72
  `
66
73
  fn outline_mask(rect: vec2<u32>, scale_offset: vec2<f32>, at: vec2<u32>) -> f32 {
@@ -68,7 +75,12 @@ fn outline_mask(rect: vec2<u32>, scale_offset: vec2<f32>, at: vec2<u32>) -> f32
68
75
 
69
76
  return outline_mask_factor(m, scale_offset);
70
77
  }
78
+ `, [chunk_outline_profile]
79
+ );
71
80
 
81
+ const body = CodeChunk.from(
82
+ //language=WGSL
83
+ `
72
84
  fn outline_label_of(seed: u32) -> u32 {
73
85
  return textureLoad(labels, jump_flood_seed_unpack(seed), 0).r;
74
86
  }
@@ -95,7 +107,7 @@ fn main(
95
107
  let delta = 0.5 * (settings.upscale.x + settings.upscale.y);
96
108
 
97
109
  // a covered pixel shows its own fill and nothing else
98
- if (textureLoad(tile_seeds, pixel / ${OUTLINE_TILE_SIZE}, 0).r != 0u) {
110
+ if (textureLoad(tile_labels, pixel / ${OUTLINE_TILE_SIZE}, 0).r != OUTLINE_NONE) {
99
111
  let label = textureLoad(labels, pixel, 0).r;
100
112
 
101
113
  if (label != JUMP_FLOOD_NONE) {
@@ -129,15 +141,39 @@ fn main(
129
141
 
130
142
  return outline_nearer_over(near, far, near_distance, far_distance, delta);
131
143
  }
132
- `, [chunk_jump_flood_seed_pair, chunk_outline_profile]
144
+ `, [chunk_jump_flood_seed_pair, chunk_outline_profile, chunk_outline_mask, chunk_outline_label_words]
133
145
  );
134
146
 
135
147
  /**
136
- * Paints the outlines and fills over the scene colour, on the active tiles only.
148
+ * The scene colour, blended premultiplied: `one, one-minus-src-alpha` over what is there.
149
+ *
150
+ * @type {GPUColorTargetState[]}
151
+ */
152
+ export const OUTLINE_COMPOSITE_TARGETS = [
153
+ {
154
+ label: "color",
155
+ format: "rgba16float",
156
+ blend: BlendState.from({
157
+ color: {
158
+ operation: "add",
159
+ srcFactor: "one",
160
+ dstFactor: "one-minus-src-alpha"
161
+ },
162
+ alpha: {
163
+ operation: "add",
164
+ srcFactor: "one",
165
+ dstFactor: "one-minus-src-alpha"
166
+ }
167
+ })
168
+ }
169
+ ];
170
+
171
+ /**
172
+ * Paints the outlines and fills over the scene colour, on the flood's tiles.
137
173
  *
138
174
  * A pixel some spec covers shows that spec's fill; any other pixel shows the outlines of the two
139
- * nearest specs, nearer on top (see {@link chunk_outline_profile}). Both are premultiplied and blend
140
- * `one, one-minus-src-alpha` over what is there.
175
+ * nearest specs, nearer on top (see {@link chunk_outline_profile}). Both are premultiplied. The tiles
176
+ * the flood left out show a fill alone, and are drawn by `shader_outline_fill`.
141
177
  *
142
178
  * @type {ScreenTileShader}
143
179
  */
@@ -148,22 +184,5 @@ export const shader_outline_composite = new ScreenTileShader({
148
184
  body
149
185
  }),
150
186
  vertex: chunk_outline_tile_vertex,
151
- targets: [
152
- {
153
- label: "color",
154
- format: "rgba16float",
155
- blend: BlendState.from({
156
- color: {
157
- operation: "add",
158
- srcFactor: "one",
159
- dstFactor: "one-minus-src-alpha"
160
- },
161
- alpha: {
162
- operation: "add",
163
- srcFactor: "one",
164
- dstFactor: "one-minus-src-alpha"
165
- }
166
- })
167
- }
168
- ]
187
+ targets: OUTLINE_COMPOSITE_TARGETS
169
188
  });
@@ -0,0 +1,59 @@
1
+ import { COLLECTION_U32_STRUCT_READ } from "../../../shade/renderer/gpu_primitive/COLLECTION_U32_STRUCT_READ.js";
2
+ import { CodeChunk } from "../../../shade/renderer/shader/compiler/CodeChunk.js";
3
+ import { ShaderResourceSetDescriptor } from "../../../shade/renderer/shader/resource/ShaderResourceSetDescriptor.js";
4
+ import { ShaderDescriptor } from "../../../shade/renderer/shader/ShaderDescriptor.js";
5
+ import { ScreenTileShader } from "../../../shade/renderer/shader/tile/ScreenTileShader.js";
6
+ import { WebGPUArray } from "../../../shade/renderer/shader/type/WebGPUArray.js";
7
+ import { chunk_outline_profile } from "./chunk_outline_profile.js";
8
+ import { OUTLINE_SPEC_STRUCT } from "./OUTLINE_SPEC_STRUCT.js";
9
+ import { OUTLINE_TILE_SIZE } from "./OUTLINE_TILE_SIZE.js";
10
+ import {
11
+ chunk_outline_mask,
12
+ chunk_outline_tile_vertex,
13
+ OUTLINE_COMPOSITE_SETTINGS,
14
+ OUTLINE_COMPOSITE_TARGETS
15
+ } from "./shader_outline_composite.js";
16
+
17
+ const resources = new ShaderResourceSetDescriptor();
18
+
19
+ resources.setImmediate("settings", OUTLINE_COMPOSITE_SETTINGS);
20
+
21
+ resources.createGroup()
22
+ .addStorageBuffer("tiles", COLLECTION_U32_STRUCT_READ)
23
+ .addTexture("tile_labels", "uint")
24
+ .addStorageBuffer("specs", WebGPUArray.from(OUTLINE_SPEC_STRUCT))
25
+ .addTexture("mask_atlas", "unfilterable-float");
26
+
27
+ /**
28
+ * Paints the fill over the tiles the flood left out, listed by `shader_outline_fill_tiles`: every
29
+ * pixel of such a tile holds the tile's one label and no other label is within reach, so it shows
30
+ * that label's fill and nothing else — what `shader_outline_composite` would draw there, without the
31
+ * flood's state.
32
+ *
33
+ * @type {ScreenTileShader}
34
+ */
35
+ export const shader_outline_fill = new ScreenTileShader({
36
+ descriptor: ShaderDescriptor.from({
37
+ label: "outline / fill",
38
+ resources,
39
+ body: CodeChunk.from(
40
+ //language=WGSL
41
+ `
42
+ @fragment
43
+ fn main(
44
+ @builtin(position) coord : vec4<f32>,
45
+ ) -> @location(0) vec4<f32> {
46
+ let pixel = vec2<i32>(coord.xy);
47
+
48
+ let spec = specs[textureLoad(tile_labels, pixel / ${OUTLINE_TILE_SIZE}, 0).r];
49
+
50
+ let mask_at = outline_mask_coordinate(pixel, settings.jitter, settings.upscale);
51
+
52
+ return outline_fill(spec) * outline_mask(spec.fill_mask_rect, spec.fill_mask_fma, mask_at);
53
+ }
54
+ `, [chunk_outline_profile, chunk_outline_mask]
55
+ )
56
+ }),
57
+ vertex: chunk_outline_tile_vertex,
58
+ targets: OUTLINE_COMPOSITE_TARGETS
59
+ });
@@ -0,0 +1,70 @@
1
+ import { COLLECTION_U32_STRUCT_WRITE } from "../../../shade/renderer/gpu_primitive/COLLECTION_U32_STRUCT_WRITE.js";
2
+ import { CodeChunk } from "../../../shade/renderer/shader/compiler/CodeChunk.js";
3
+ import { ComputeShader } from "../../../shade/renderer/shader/ComputeShader.js";
4
+ import { ShaderResourceSetDescriptor } from "../../../shade/renderer/shader/resource/ShaderResourceSetDescriptor.js";
5
+ import { WebGPUArray } from "../../../shade/renderer/shader/type/WebGPUArray.js";
6
+ import { chunk_outline_label_words } from "./chunk_outline_label_words.js";
7
+ import { OUTLINE_SPEC_STRUCT } from "./OUTLINE_SPEC_STRUCT.js";
8
+ import { OUTLINE_TILE_GRID_GROUP_SIZE, OUTLINE_TILE_GRID_SETTINGS } from "./shader_outline_tile_edges.js";
9
+
10
+ const resources = new ShaderResourceSetDescriptor();
11
+
12
+ resources.setImmediate("settings", OUTLINE_TILE_GRID_SETTINGS);
13
+
14
+ resources.createGroup()
15
+ .addTexture("tile_labels", "uint")
16
+ .addTexture("tile_active", "uint")
17
+ .addStorageBuffer("specs", WebGPUArray.from(OUTLINE_SPEC_STRUCT));
18
+
19
+ resources.createGroup()
20
+ .addStorageBuffer("fill_tiles", COLLECTION_U32_STRUCT_WRITE, true);
21
+
22
+ /**
23
+ * Lists the tiles the fill pass draws: those the flood left out whose one label has a fill to draw,
24
+ * packed `x | y << 16`, in no particular order.
25
+ *
26
+ * A tile outside the flood has no other label within reach (else an edge tile of that label would be
27
+ * within reach of it, and the flood would hold it), so its pixels show their own label's fill and
28
+ * nothing else. A mixed tile is always an edge, so always in the flood; a tile with no label shows
29
+ * nothing; and a fill of zero alpha draws nothing, so its tiles are not listed at all.
30
+ *
31
+ * The list's count has to start at zero, and the list must hold every tile of the grid.
32
+ *
33
+ * @type {ComputeShader}
34
+ */
35
+ export const shader_outline_fill_tiles = ComputeShader.from({
36
+ label: "outline / fill tiles",
37
+ resources,
38
+ body: CodeChunk.from(
39
+ //language=WGSL
40
+ `
41
+ @compute @workgroup_size(${OUTLINE_TILE_GRID_GROUP_SIZE}, ${OUTLINE_TILE_GRID_GROUP_SIZE}, 1)
42
+ fn main(@builtin(global_invocation_id) id: vec3<u32>) {
43
+ if (any(id.xy >= settings.resolution)) {
44
+ return;
45
+ }
46
+
47
+ let cell = vec2<i32>(id.xy);
48
+
49
+ if (textureLoad(tile_active, cell, 0).r != 0u) {
50
+ return;
51
+ }
52
+
53
+ let label = textureLoad(tile_labels, cell, 0).r;
54
+
55
+ // one label: the reserved words are the two largest
56
+ if (label >= OUTLINE_TILE_MIXED) {
57
+ return;
58
+ }
59
+
60
+ if (specs[label].fill_color.a <= 0.0) {
61
+ return;
62
+ }
63
+
64
+ let slot = atomicAdd(&fill_tiles.count, 1u);
65
+
66
+ fill_tiles.elements[slot] = id.x | (id.y << 16u);
67
+ }
68
+ `, [chunk_outline_label_words]
69
+ )
70
+ });
@@ -6,6 +6,7 @@ import { ComputeShader } from "../../../shade/renderer/shader/ComputeShader.js";
6
6
  import { ShaderResourceSetDescriptor } from "../../../shade/renderer/shader/resource/ShaderResourceSetDescriptor.js";
7
7
  import { u32, vec2u } from "../../../shade/renderer/shader/type/WEBGPU_WGSL_PRIMITIVE_TYPES.js";
8
8
  import { WebGPUStruct } from "../../../shade/renderer/shader/type/WebGPUStruct.js";
9
+ import { chunk_outline_label_words } from "./chunk_outline_label_words.js";
9
10
  import { chunk_outline_tile_pixel } from "./chunk_outline_tile_pixel.js";
10
11
  import { OUTLINE_TILE_SIZE } from "./OUTLINE_TILE_SIZE.js";
11
12
 
@@ -23,7 +24,7 @@ resources.setImmediate("settings", OUTLINE_FLOOD_SETTINGS);
23
24
 
24
25
  resources.createGroup()
25
26
  .addTexture("labels", "uint")
26
- .addTexture("tile_seeds", "uint")
27
+ .addTexture("tile_labels", "uint")
27
28
  .addStorageBuffer("tiles", COLLECTION_U32_STRUCT_READ);
28
29
 
29
30
  resources.createGroup()
@@ -34,7 +35,8 @@ resources.createGroup()
34
35
  * is a seed, at its own position. Writes each active pixel's two seeds (see
35
36
  * {@link chunk_jump_flood_seed_pair}) for {@link shader_outline_flood_step} to refine.
36
37
  *
37
- * Labels are valid only inside tiles that hold one, so a tap is read only where its tile says so.
38
+ * Labels are valid only inside tiles that hold one, so a tap is read only where its tile's label word
39
+ * says so.
38
40
  *
39
41
  * @type {ComputeShader}
40
42
  */
@@ -66,7 +68,7 @@ fn main(
66
68
  continue;
67
69
  }
68
70
 
69
- if (textureLoad(tile_seeds, at / ${OUTLINE_TILE_SIZE}, 0).r == 0u) {
71
+ if (textureLoad(tile_labels, at / ${OUTLINE_TILE_SIZE}, 0).r == OUTLINE_NONE) {
70
72
  continue;
71
73
  }
72
74
 
@@ -83,6 +85,6 @@ fn main(
83
85
 
84
86
  textureStore(state_out, pixel, vec4<u32>(pair.seed_0, pair.seed_1, 0u, 0u));
85
87
  }
86
- `, [chunk_jump_flood_tap, chunk_jump_flood_seed_pair, chunk_outline_tile_pixel]
88
+ `, [chunk_jump_flood_tap, chunk_jump_flood_seed_pair, chunk_outline_tile_pixel, chunk_outline_label_words]
87
89
  )
88
90
  });
@@ -0,0 +1,92 @@
1
+ import { chunk_jump_flood_tap } from "../../../shade/renderer/gpu_primitive/jump_flood/chunk_jump_flood_tap.js";
2
+ import { CodeChunk } from "../../../shade/renderer/shader/compiler/CodeChunk.js";
3
+ import { ComputeShader } from "../../../shade/renderer/shader/ComputeShader.js";
4
+ import { ShaderResourceSetDescriptor } from "../../../shade/renderer/shader/resource/ShaderResourceSetDescriptor.js";
5
+ import { vec2u } from "../../../shade/renderer/shader/type/WEBGPU_WGSL_PRIMITIVE_TYPES.js";
6
+ import { WebGPUStruct } from "../../../shade/renderer/shader/type/WebGPUStruct.js";
7
+ import { chunk_outline_label_words } from "./chunk_outline_label_words.js";
8
+
9
+ /**
10
+ * Tiles per workgroup side, for the passes that run over the tile grid one tile a lane.
11
+ *
12
+ * @type {number}
13
+ */
14
+ export const OUTLINE_TILE_GRID_GROUP_SIZE = 8;
15
+
16
+ export const OUTLINE_TILE_GRID_SETTINGS = WebGPUStruct.from({
17
+ /**
18
+ * The tile grid's size, in tiles.
19
+ */
20
+ resolution: vec2u,
21
+ });
22
+
23
+ const resources = new ShaderResourceSetDescriptor();
24
+
25
+ resources.setImmediate("settings", OUTLINE_TILE_GRID_SETTINGS);
26
+
27
+ resources.createGroup()
28
+ .addTexture("tile_labels", "uint");
29
+
30
+ resources.createGroup()
31
+ .addStorageTexture("edges", "r32uint", "2d", "write-only");
32
+
33
+ /**
34
+ * Marks the tiles a silhouette passes through, from the classify pass's tile label words: the tiles
35
+ * the jump flood has to start from.
36
+ *
37
+ * The flood looks for each pixel's nearest pixel of a label, and that pixel always lies on the
38
+ * label's boundary. Were all eight neighbours of a pixel `q` of label `L` also `L`, then for any pixel
39
+ * `p` outside `L` the neighbour one step from `q` towards `p` would be `L` and nearer to `p`; so the
40
+ * nearest `q` has a neighbour of another label, and that neighbour lies in the box between `q` and `p`,
41
+ * so on the screen. The tile holding `q` therefore holds another label too, or touches a tile that
42
+ * does:
43
+ *
44
+ * - a tile whose word is `OUTLINE_TILE_MIXED` is an edge;
45
+ * - a tile of one label is an edge when any of its eight neighbours on the grid has another word —
46
+ * including a silhouette that runs along tile borders, where neither side is mixed;
47
+ * - a tile with no label holds no seed and is never an edge.
48
+ *
49
+ * Tiles off the grid do not count: the neighbour above lies on the screen.
50
+ *
51
+ * @type {ComputeShader}
52
+ */
53
+ export const shader_outline_tile_edges = ComputeShader.from({
54
+ label: "outline / tile edges",
55
+ resources,
56
+ body: CodeChunk.from(
57
+ //language=WGSL
58
+ `
59
+ @compute @workgroup_size(${OUTLINE_TILE_GRID_GROUP_SIZE}, ${OUTLINE_TILE_GRID_GROUP_SIZE}, 1)
60
+ fn main(@builtin(global_invocation_id) id: vec3<u32>) {
61
+ if (any(id.xy >= settings.resolution)) {
62
+ return;
63
+ }
64
+
65
+ let cell = vec2<i32>(id.xy);
66
+
67
+ let own = textureLoad(tile_labels, cell, 0).r;
68
+
69
+ var edge = select(0u, 1u, own == OUTLINE_TILE_MIXED);
70
+
71
+ // one label: the reserved words are the two largest
72
+ if (own < OUTLINE_TILE_MIXED) {
73
+ let extent = vec2<i32>(settings.resolution);
74
+
75
+ for (var tap = 0u; tap < JUMP_FLOOD_TAP_COUNT; tap++) {
76
+ let at = cell + jump_flood_tap(tap, 1u);
77
+
78
+ if (any(at < vec2<i32>(0)) || any(at >= extent)) {
79
+ continue;
80
+ }
81
+
82
+ if (textureLoad(tile_labels, at, 0).r != own) {
83
+ edge = 1u;
84
+ }
85
+ }
86
+ }
87
+
88
+ textureStore(edges, cell, vec4<u32>(edge, 0u, 0u, 0u));
89
+ }
90
+ `, [chunk_jump_flood_tap, chunk_outline_label_words]
91
+ )
92
+ });