@woosh/meep-engine 3.60.9 → 3.61.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.
Files changed (109) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/package.json +1 -1
  3. package/src/engine/ecs/EntityManager.d.ts +3 -3
  4. package/src/engine/ecs/EntityManager.js +3 -3
  5. package/src/engine/ecs/storage/populateEngineSerializationRegistry.d.ts.map +1 -1
  6. package/src/engine/ecs/storage/populateEngineSerializationRegistry.js +4 -0
  7. package/src/engine/graphics/ecs/decal/v2/Decal.d.ts +46 -2
  8. package/src/engine/graphics/ecs/decal/v2/Decal.d.ts.map +1 -1
  9. package/src/engine/graphics/ecs/decal/v2/Decal.js +74 -5
  10. package/src/engine/graphics/ecs/decal/v2/DecalSerializationAdapter.d.ts.map +1 -1
  11. package/src/engine/graphics/ecs/decal/v2/DecalSerializationAdapter.js +13 -1
  12. package/src/engine/graphics/ecs/decal/v2/DecalSerializationUpgrader_1_2.d.ts +9 -0
  13. package/src/engine/graphics/ecs/decal/v2/DecalSerializationUpgrader_1_2.d.ts.map +1 -0
  14. package/src/engine/graphics/ecs/decal/v2/DecalSerializationUpgrader_1_2.js +37 -0
  15. package/src/engine/graphics3/DecalSystem.d.ts.map +1 -1
  16. package/src/engine/graphics3/DecalSystem.js +462 -447
  17. package/src/engine/graphics3/decal/DECAL_GPU_RECORD.d.ts +14 -0
  18. package/src/engine/graphics3/decal/DECAL_GPU_RECORD.d.ts.map +1 -1
  19. package/src/engine/graphics3/decal/DECAL_GPU_RECORD.js +53 -13
  20. package/src/engine/graphics3/decal/chunk_decal_record.d.ts +6 -1
  21. package/src/engine/graphics3/decal/chunk_decal_record.d.ts.map +1 -1
  22. package/src/engine/graphics3/decal/chunk_decal_record.js +28 -9
  23. package/src/engine/graphics3/decal/decal_gpu_records.d.ts +2 -1
  24. package/src/engine/graphics3/decal/decal_gpu_records.d.ts.map +1 -1
  25. package/src/engine/graphics3/decal/decal_gpu_records.js +26 -5
  26. package/src/engine/graphics3/decal/decal_material_samplers.d.ts +6 -0
  27. package/src/engine/graphics3/decal/decal_material_samplers.d.ts.map +1 -1
  28. package/src/engine/graphics3/decal/decal_material_samplers.js +13 -0
  29. package/src/engine/graphics3/decal/shader_cluster_assign_decals.js +1 -1
  30. package/src/engine/graphics3/decal/shader_cull_decals_frustum.js +1 -1
  31. package/src/engine/graphics3/decal/shader_decal_composite.d.ts +5 -0
  32. package/src/engine/graphics3/decal/shader_decal_composite.d.ts.map +1 -1
  33. package/src/engine/graphics3/decal/shader_decal_composite.js +69 -28
  34. package/src/engine/network/NetworkSession.d.ts +3 -0
  35. package/src/engine/network/NetworkSession.d.ts.map +1 -1
  36. package/src/engine/network/NetworkSession.js +68 -10
  37. package/src/engine/network/README.md +6 -3
  38. package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts +51 -5
  39. package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts.map +1 -1
  40. package/src/engine/network/orchestrator/ServerAuthoritativeClient.js +308 -34
  41. package/src/engine/network/replication/Replicator.d.ts +8 -2
  42. package/src/engine/network/replication/Replicator.d.ts.map +1 -1
  43. package/src/engine/network/replication/Replicator.js +13 -4
  44. package/src/engine/network/sim/ActionLog.d.ts +16 -2
  45. package/src/engine/network/sim/ActionLog.d.ts.map +1 -1
  46. package/src/engine/network/sim/ActionLog.js +33 -2
  47. package/src/engine/network/sim/RewindEngine.d.ts +9 -0
  48. package/src/engine/network/sim/RewindEngine.d.ts.map +1 -1
  49. package/src/engine/network/sim/RewindEngine.js +15 -1
  50. package/src/engine/network/state/FrameBufferRing.d.ts +12 -5
  51. package/src/engine/network/state/FrameBufferRing.d.ts.map +1 -1
  52. package/src/engine/network/state/FrameBufferRing.js +32 -6
  53. package/src/engine/network/time/TimeDilation.d.ts +30 -14
  54. package/src/engine/network/time/TimeDilation.d.ts.map +1 -1
  55. package/src/engine/network/time/TimeDilation.js +47 -17
  56. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  57. package/src/generation/foliage/skeleton/ShootWoodHash.d.ts +3 -2
  58. package/src/generation/foliage/skeleton/ShootWoodHash.d.ts.map +1 -1
  59. package/src/generation/foliage/skeleton/ShootWoodHash.js +25 -2
  60. package/src/shade/renderer/buffer/graph_read_buffer.d.ts +3 -1
  61. package/src/shade/renderer/buffer/graph_read_buffer.d.ts.map +1 -1
  62. package/src/shade/renderer/buffer/graph_read_buffer.js +9 -6
  63. package/src/shade/renderer/buffer/util/shader_buffer_copy_storage_to_storage.d.ts +5 -0
  64. package/src/shade/renderer/buffer/util/shader_buffer_copy_storage_to_storage.d.ts.map +1 -1
  65. package/src/shade/renderer/buffer/util/shader_buffer_copy_storage_to_storage.js +48 -43
  66. package/src/shade/renderer/material/GPUMaterialManager.d.ts.map +1 -1
  67. package/src/shade/renderer/material/GPUMaterialManager.js +117 -35
  68. package/src/shade/renderer/material/standard/fragment_gbuffer.d.ts +2 -2
  69. package/src/shade/renderer/material/standard/fragment_gbuffer.d.ts.map +1 -1
  70. package/src/shade/renderer/material/standard/fragment_gbuffer.js +10 -3
  71. package/src/shade/renderer/material/standard/material_gbuffer_vertex.d.ts +15 -3
  72. package/src/shade/renderer/material/standard/material_gbuffer_vertex.d.ts.map +1 -1
  73. package/src/shade/renderer/material/standard/material_gbuffer_vertex.js +25 -4
  74. package/src/shade/renderer/particles/GPUParticleSystem.d.ts +23 -5
  75. package/src/shade/renderer/particles/GPUParticleSystem.d.ts.map +1 -1
  76. package/src/shade/renderer/particles/GPUParticleSystem.js +294 -20
  77. package/src/shade/renderer/particles/resize/shader_particle_shrink_alive_list.d.ts +21 -0
  78. package/src/shade/renderer/particles/resize/shader_particle_shrink_alive_list.d.ts.map +1 -0
  79. package/src/shade/renderer/particles/resize/shader_particle_shrink_alive_list.js +83 -0
  80. package/src/shade/renderer/particles/resize/shader_particle_shrink_free_list.d.ts +21 -0
  81. package/src/shade/renderer/particles/resize/shader_particle_shrink_free_list.d.ts.map +1 -0
  82. package/src/shade/renderer/particles/resize/shader_particle_shrink_free_list.js +64 -0
  83. package/src/shade/renderer/restir/di/chunk_restir_resolve.d.ts.map +1 -1
  84. package/src/shade/renderer/restir/di/chunk_restir_resolve.js +91 -94
  85. package/src/shade/renderer/restir/reservoir/chunk_rtxdi_stream_sample.js +1 -1
  86. package/src/shade/renderer/shader/chunk/color/rgbe9995/chunk_rgbe9995_decode.d.ts +14 -0
  87. package/src/shade/renderer/shader/chunk/color/rgbe9995/chunk_rgbe9995_decode.d.ts.map +1 -1
  88. package/src/shade/renderer/shader/chunk/color/rgbe9995/chunk_rgbe9995_decode.js +36 -21
  89. package/src/shade/renderer/shader/chunk/color/rgbe9995/chunk_rgbe9995_sample_gather.d.ts +22 -0
  90. package/src/shade/renderer/shader/chunk/color/rgbe9995/chunk_rgbe9995_sample_gather.d.ts.map +1 -0
  91. package/src/shade/renderer/shader/chunk/color/rgbe9995/chunk_rgbe9995_sample_gather.js +64 -0
  92. package/src/shade/renderer/shader/chunk/geometry/normal/chunk_decode_vertex_normal.d.ts.map +1 -1
  93. package/src/shade/renderer/shader/chunk/geometry/normal/chunk_decode_vertex_normal.js +13 -18
  94. package/src/shade/renderer/shader/chunk/geometry/normal/chunk_encode_vertex_normal.d.ts.map +1 -1
  95. package/src/shade/renderer/shader/chunk/geometry/normal/chunk_encode_vertex_normal.js +28 -30
  96. package/src/shade/renderer/shader/chunk/geometry/vec2/chunk_unit_u32_to_vec2.d.ts.map +1 -1
  97. package/src/shade/renderer/shader/chunk/geometry/vec2/chunk_unit_u32_to_vec2.js +15 -16
  98. package/src/shade/renderer/shader/chunk/geometry/vec2/chunk_unit_vec2_to_u32.d.ts +0 -1
  99. package/src/shade/renderer/shader/chunk/geometry/vec2/chunk_unit_vec2_to_u32.d.ts.map +1 -1
  100. package/src/shade/renderer/shader/chunk/geometry/vec2/chunk_unit_vec2_to_u32.js +15 -18
  101. package/src/shade/renderer/shader/chunk/subgroup/chunk_subgroup_bitset_or.d.ts +32 -0
  102. package/src/shade/renderer/shader/chunk/subgroup/chunk_subgroup_bitset_or.d.ts.map +1 -0
  103. package/src/shade/renderer/shader/chunk/subgroup/chunk_subgroup_bitset_or.js +73 -0
  104. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +8 -1
  105. package/src/shade/wgsl/emulator/WGSLLanguageCore.d.ts.map +1 -1
  106. package/src/shade/wgsl/emulator/WGSLLanguageCore.js +7 -3
  107. package/src/shade/renderer/global_illumination/brick4/cpu/brick4_sh3_color_encode.d.ts +0 -2
  108. package/src/shade/renderer/global_illumination/brick4/cpu/brick4_sh3_color_encode.d.ts.map +0 -1
  109. package/src/shade/renderer/global_illumination/brick4/cpu/brick4_sh3_color_encode.js +0 -6
@@ -108,13 +108,16 @@ export class ActionLog {
108
108
 
109
109
  /**
110
110
  * Open a frame for writing after the records it already holds, rather than over them. A frame
111
- * the ring does not hold is opened empty, as {@link begin_frame} opens it.
111
+ * the ring does not hold is opened empty, as {@link begin_frame} opens it — unless the log has
112
+ * moved past it, its slot holding a later frame: the later frame keeps its records, and what is
113
+ * written for this one is dropped at {@link end_frame}.
112
114
  *
113
115
  * For a writer adding to a frame someone else wrote: records arriving from a peer for a frame
114
116
  * this peer has already simulated. Opened with {@link begin_frame} instead, they would erase
115
117
  * this peer's own records for the frame — and with them the prior state a rewind needs to undo
116
118
  * that input, so a replay would apply it a second time, and the outbound stream, which packs
117
- * from this log, would lose it before the peer it was for had confirmed it.
119
+ * from this log, would lose it before the peer it was for had confirmed it. A frame the log has
120
+ * moved past would erase the later frame's records the same way.
118
121
  *
119
122
  * @param {number} frame absolute frame number
120
123
  */
@@ -127,6 +130,34 @@ export class ActionLog {
127
130
  this.#current_frame = frame;
128
131
  }
129
132
 
133
+ /**
134
+ * Open a frame the log holds for writing from byte `offset` of what it holds: the records from
135
+ * there on are dropped, and what is written takes their place.
136
+ *
137
+ * For a writer that has taken records back off the world and writes them again — the frame's
138
+ * earlier records stay, the later ones are written over.
139
+ *
140
+ * @param {number} frame absolute frame number
141
+ * @param {number} offset a record boundary in the frame, at most its end
142
+ */
143
+ resume_frame_at(frame, offset) {
144
+ if (this.#current_frame !== -1) {
145
+ throw new Error(`ActionLog.resume_frame_at: previous frame ${this.#current_frame} still open; call end_frame first`);
146
+ }
147
+ if (!this.#ring.has(frame)) {
148
+ throw new Error(`ActionLog.resume_frame_at: frame ${frame} is not present in the log`);
149
+ }
150
+
151
+ const buffer = this.#ring.resume(frame);
152
+ if (offset > buffer.position) {
153
+ throw new Error(`ActionLog.resume_frame_at: offset ${offset} is past frame ${frame}'s end ${buffer.position}`);
154
+ }
155
+ buffer.position = offset;
156
+
157
+ this.#current_buffer = buffer;
158
+ this.#current_frame = frame;
159
+ }
160
+
130
161
  /**
131
162
  * Drop every record the log holds, keeping each frame it holds present and empty.
132
163
  *
@@ -50,6 +50,15 @@ export class RewindEngine {
50
50
  * @param {number} frame
51
51
  */
52
52
  undo_frame(frame: number): void;
53
+ /**
54
+ * Undo the records of a frame that start at byte `from` or later, in reverse order — the ones
55
+ * written to the frame from there on — and leave the ones before it applied.
56
+ * Throws if the frame is not present in the log.
57
+ *
58
+ * @param {number} frame
59
+ * @param {number} from offset of a record boundary in the frame
60
+ */
61
+ undo_frame_from(frame: number, from: number): void;
53
62
  /**
54
63
  * Undo every frame in `(target_frame, current_frame]` so that the world is
55
64
  * left in start-of-`target_frame + 1` state. Equivalently: the world ends
@@ -1 +1 @@
1
- {"version":3,"file":"RewindEngine.d.ts","sourceRoot":"","sources":["../../../../../src/engine/network/sim/RewindEngine.js"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;GAmBG;AACH;IAeI;;;;;;OAMG;IACH,uDANW;QACV,UAAc,YAAY;QAC1B,KAAS,yBAAyB;QAClC,kBAAsB,8BAA8B;KACjD,EAiBH;IAdG;;OAEG;IACH,sBAA4B;IAE5B;;OAEG;IACH,8BAAkB;IAElB;;OAEG;IACH,gDAA4C;IAGhD;;;;;OAKG;IACH,kBAFW,MAAM,QA0BhB;IAED;;;;;;;;;OASG;IACH,yBAHW,MAAM,gBACN,MAAM,QAWhB;;CAuDJ"}
1
+ {"version":3,"file":"RewindEngine.d.ts","sourceRoot":"","sources":["../../../../../src/engine/network/sim/RewindEngine.js"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;GAmBG;AACH;IAeI;;;;;;OAMG;IACH,uDANW;QACV,UAAc,YAAY;QAC1B,KAAS,yBAAyB;QAClC,kBAAsB,8BAA8B;KACjD,EAiBH;IAdG;;OAEG;IACH,sBAA4B;IAE5B;;OAEG;IACH,8BAAkB;IAElB;;OAEG;IACH,gDAA4C;IAGhD;;;;;OAKG;IACH,kBAFW,MAAM,QAIhB;IAED;;;;;;;OAOG;IACH,uBAHW,MAAM,QACN,MAAM,QA4BhB;IAED;;;;;;;;;OASG;IACH,yBAHW,MAAM,gBACN,MAAM,QAWhB;;CAuDJ"}
@@ -67,9 +67,22 @@ export class RewindEngine {
67
67
  * @param {number} frame
68
68
  */
69
69
  undo_frame(frame) {
70
+ this.undo_frame_from(frame, 0);
71
+ }
72
+
73
+ /**
74
+ * Undo the records of a frame that start at byte `from` or later, in reverse order — the ones
75
+ * written to the frame from there on — and leave the ones before it applied.
76
+ * Throws if the frame is not present in the log.
77
+ *
78
+ * @param {number} frame
79
+ * @param {number} from offset of a record boundary in the frame
80
+ */
81
+ undo_frame_from(frame, from) {
70
82
  assert.isNonNegativeInteger(frame, 'frame');
83
+ assert.isNonNegativeInteger(from, 'from');
71
84
  if (!this.action_log.has_frame(frame)) {
72
- throw new Error(`RewindEngine.undo_frame: frame ${frame} is not present in the action log`);
85
+ throw new Error(`RewindEngine: frame ${frame} is not present in the action log`);
73
86
  }
74
87
 
75
88
  // 1. First pass: walk forward to find record boundaries.
@@ -77,6 +90,7 @@ export class RewindEngine {
77
90
  cursor.open(this.action_log, frame);
78
91
  let record_count = 0;
79
92
  while (cursor.next()) {
93
+ if (cursor.record_start < from) continue;
80
94
  if (record_count >= this.#record_starts.length) {
81
95
  this.#grow_record_starts();
82
96
  }
@@ -21,7 +21,9 @@
21
21
  * from that moment; its recorded length is what was last committed — 0 for a
22
22
  * slot `open` claimed, the old end for a frame `resume` reopened — so a reader
23
23
  * sees none of what is still being written. Owners that allow a frame to stay
24
- * open across calls are responsible for saying so.
24
+ * open across calls are responsible for saying so. `resume` never claims a slot
25
+ * that holds a later frame: a frame the ring has moved past is written to a
26
+ * buffer the ring does not keep.
25
27
  *
26
28
  * @author Alex Goldring
27
29
  * @copyright Company Named Limited (c) 2026
@@ -52,8 +54,12 @@ export class FrameBufferRing {
52
54
  open(frame: number): BinaryBuffer;
53
55
  /**
54
56
  * Claim the slot for `frame` to write AFTER what it already holds: a slot holding `frame`
55
- * hands back its buffer positioned at its committed end, and any other slot is claimed as
56
- * {@link open} claims it. {@link commit} then fixes the longer length.
57
+ * hands back its buffer positioned at its committed end, and a slot holding an earlier frame,
58
+ * or none, is claimed as {@link open} claims it. {@link commit} then fixes the longer length.
59
+ *
60
+ * A slot holding a later frame is not claimed: the ring has moved past `frame`, and claiming
61
+ * the slot would erase a frame newer than the one being written. What is written for `frame`
62
+ * goes to a buffer the ring does not keep, and `frame` stays absent.
57
63
  *
58
64
  * @param {number} frame absolute frame number
59
65
  * @returns {BinaryBuffer} positioned at the frame's committed end, ready to be written
@@ -69,9 +75,10 @@ export class FrameBufferRing {
69
75
  */
70
76
  clear(): void;
71
77
  /**
72
- * Record how far the writer got, fixing the frame's length for readers.
78
+ * Record how far the writer got, fixing the frame's length for readers. A frame the ring does
79
+ * not hold — one {@link resume} found it had moved past — keeps nothing.
73
80
  *
74
- * @param {number} frame absolute frame number, previously passed to {@link open}
81
+ * @param {number} frame absolute frame number, previously passed to {@link open} or {@link resume}
75
82
  */
76
83
  commit(frame: number): void;
77
84
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"FrameBufferRing.d.ts","sourceRoot":"","sources":["../../../../../src/engine/network/state/FrameBufferRing.js"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH;IAoCI;;OAEG;IACH,4DAFW;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAC;QAAC,mBAAmB,EAAE,MAAM,CAAA;KAAC,EAmB9E;IA5BD;;;;;OAKG;IACH,yBAFU,MAAM,CAED;IAwBf;;;;;;OAMG;IACH,YAHW,MAAM,GACJ,YAAY,CAkBxB;IAED;;;;;;;OAOG;IACH,cAHW,MAAM,GACJ,YAAY,CAgBxB;IAED;;;OAGG;IACH,cAEC;IAED;;OAEG;IACH,cAGC;IAED;;;;OAIG;IACH,cAFW,MAAM,QAQhB;IAED;;;;;OAKG;IACH,WAHW,MAAM,GACJ,OAAO,CAMnB;IAED;;;;;;OAMG;IACH,kBAHW,MAAM,GACJ,YAAY,CAYxB;IAED;;;;;;OAMG;IACH,qBAHW,MAAM,GACJ,MAAM,CAQlB;;CACJ;6BAnN4B,sCAAsC"}
1
+ {"version":3,"file":"FrameBufferRing.d.ts","sourceRoot":"","sources":["../../../../../src/engine/network/state/FrameBufferRing.js"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH;IA2CI;;OAEG;IACH,4DAFW;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAC;QAAC,mBAAmB,EAAE,MAAM,CAAA;KAAC,EAmB9E;IA5BD;;;;;OAKG;IACH,yBAFU,MAAM,CAED;IAwBf;;;;;;OAMG;IACH,YAHW,MAAM,GACJ,YAAY,CAkBxB;IAED;;;;;;;;;;;OAWG;IACH,cAHW,MAAM,GACJ,YAAY,CA0BxB;IAED;;;OAGG;IACH,cAEC;IAED;;OAEG;IACH,cAGC;IAED;;;;;OAKG;IACH,cAFW,MAAM,QAUhB;IAED;;;;;OAKG;IACH,WAHW,MAAM,GACJ,OAAO,CAMnB;IAED;;;;;;OAMG;IACH,kBAHW,MAAM,GACJ,YAAY,CAYxB;IAED;;;;;;OAMG;IACH,qBAHW,MAAM,GACJ,MAAM,CAQlB;;CACJ;6BA7O4B,sCAAsC"}
@@ -24,7 +24,9 @@ import { BinaryBuffer } from "../../../core/binary/BinaryBuffer.js";
24
24
  * from that moment; its recorded length is what was last committed — 0 for a
25
25
  * slot `open` claimed, the old end for a frame `resume` reopened — so a reader
26
26
  * sees none of what is still being written. Owners that allow a frame to stay
27
- * open across calls are responsible for saying so.
27
+ * open across calls are responsible for saying so. `resume` never claims a slot
28
+ * that holds a later frame: a frame the ring has moved past is written to a
29
+ * buffer the ring does not keep.
28
30
  *
29
31
  * @author Alex Goldring
30
32
  * @copyright Company Named Limited (c) 2026
@@ -57,6 +59,13 @@ export class FrameBufferRing {
57
59
  */
58
60
  #write_ends;
59
61
 
62
+ /**
63
+ * What {@link resume} hands the writer of a frame the ring has moved past: rewound for each
64
+ * writer, never read and never kept.
65
+ * @type {BinaryBuffer}
66
+ */
67
+ #discard = new BinaryBuffer();
68
+
60
69
  /**
61
70
  * Number of frames retained. Writing frame `f` evicts frame
62
71
  * `f - frame_capacity`.
@@ -114,8 +123,12 @@ export class FrameBufferRing {
114
123
 
115
124
  /**
116
125
  * Claim the slot for `frame` to write AFTER what it already holds: a slot holding `frame`
117
- * hands back its buffer positioned at its committed end, and any other slot is claimed as
118
- * {@link open} claims it. {@link commit} then fixes the longer length.
126
+ * hands back its buffer positioned at its committed end, and a slot holding an earlier frame,
127
+ * or none, is claimed as {@link open} claims it. {@link commit} then fixes the longer length.
128
+ *
129
+ * A slot holding a later frame is not claimed: the ring has moved past `frame`, and claiming
130
+ * the slot would erase a frame newer than the one being written. What is written for `frame`
131
+ * goes to a buffer the ring does not keep, and `frame` stays absent.
119
132
  *
120
133
  * @param {number} frame absolute frame number
121
134
  * @returns {BinaryBuffer} positioned at the frame's committed end, ready to be written
@@ -125,7 +138,17 @@ export class FrameBufferRing {
125
138
 
126
139
  const slot = frame % this.frame_capacity;
127
140
 
128
- if (this.#frame_numbers[slot] !== frame) {
141
+ const occupant = this.#frame_numbers[slot];
142
+
143
+ if (occupant > frame) {
144
+ const discard = this.#discard;
145
+
146
+ discard.position = 0;
147
+
148
+ return discard;
149
+ }
150
+
151
+ if (occupant !== frame) {
129
152
  return this.open(frame);
130
153
  }
131
154
 
@@ -153,15 +176,18 @@ export class FrameBufferRing {
153
176
  }
154
177
 
155
178
  /**
156
- * Record how far the writer got, fixing the frame's length for readers.
179
+ * Record how far the writer got, fixing the frame's length for readers. A frame the ring does
180
+ * not hold — one {@link resume} found it had moved past — keeps nothing.
157
181
  *
158
- * @param {number} frame absolute frame number, previously passed to {@link open}
182
+ * @param {number} frame absolute frame number, previously passed to {@link open} or {@link resume}
159
183
  */
160
184
  commit(frame) {
161
185
  assert.isNonNegativeInteger(frame, 'frame');
162
186
 
163
187
  const slot = frame % this.frame_capacity;
164
188
 
189
+ if (this.#frame_numbers[slot] !== frame) return;
190
+
165
191
  this.#write_ends[slot] = this.#buffers[slot].position;
166
192
  }
167
193
 
@@ -1,20 +1,26 @@
1
1
  /**
2
- * Server-side computation of "tell client to run a bit faster or slower."
2
+ * How fast a client runs, from where its input lands at the host.
3
3
  *
4
- * The server maintains a target depth for its per-client input buffer. When the
5
- * buffer is too shallow (client's inputs aren't arriving fast enough), the
6
- * server tells the client to run the next few ticks at a slightly compressed
7
- * tick duration ("speed up"). When the buffer is too deep (client is sending
8
- * inputs faster than the server consumes them), the server asks for stretching.
9
- *
10
- * This is the same primitive used in Overwatch ("time dilation") and Rocket
11
- * League. Adjustment is bounded to a few percent so it's imperceptible.
12
- *
13
- * Output is a multiplier on tick duration:
4
+ * The host reports, each tick, how many frames ahead of its simulation the client's newest frame is:
5
+ * the depth of its input buffer for that client. The client wants it at {@link target_buffer_depth},
6
+ * deep enough that its input is there when the host needs it and no deeper, since every frame of it is
7
+ * a frame of latency. {@link compute} turns the depth into a multiplier on the client's tick duration:
14
8
  * factor < 1.0 → run faster (smaller tick interval)
15
9
  * factor > 1.0 → run slower (larger tick interval)
16
10
  * factor = 1.0 → run at nominal rate
17
11
  *
12
+ * Behind, and up to `max_dilation × T` frames ahead, an error is steered at up to {@link max_dilation}:
13
+ * a few percent, too little to see, which is what jitter and clock drift need. This is the primitive
14
+ * Overwatch and Rocket League call time dilation. A larger lead — the host lost time: a hidden or
15
+ * hitched host tab — is repaid in proportion to its size: the client runs at `1 / (1 + lead / T)` of
16
+ * its rate, which nears a standstill and never reaches it, so it keeps stepping and reading the host's
17
+ * reports. The reports are a round trip old, and a response in proportion to an error that old repays
18
+ * it without overshooting when its time constant is at least `e` times the delay, so `T` is
19
+ * `e × max_round_trip_frames` — 87 frames for a 32-frame round trip, which puts the steering's reach
20
+ * at 4.35 frames: on every round trip up to that one, a lead is repaid to the end the steering reaches
21
+ * on its own. Behind, the steering is all there is: running fast costs steps, and a client a whole
22
+ * window behind re-takes the host's time instead (`NetworkSession`).
23
+ *
18
24
  * @author Alex Goldring
19
25
  * @copyright Company Named Limited (c) 2026
20
26
  */
@@ -24,12 +30,18 @@ export class TimeDilation {
24
30
  * target_buffer_depth?: number,
25
31
  * max_dilation?: number,
26
32
  * gain?: number,
27
- * }} [options]
33
+ * max_round_trip_frames: number,
34
+ * }} options
35
+ *
36
+ * `max_round_trip_frames`: the longest round trip, in frames, the client runs on — how old, at
37
+ * most, the reports this works from are. Its owners pass its action log's depth: on a longer round
38
+ * trip the host's frames land past the log.
28
39
  */
29
- constructor({ target_buffer_depth, max_dilation, gain }?: {
40
+ constructor({ target_buffer_depth, max_dilation, gain, max_round_trip_frames }?: {
30
41
  target_buffer_depth?: number;
31
42
  max_dilation?: number;
32
43
  gain?: number;
44
+ max_round_trip_frames: number;
33
45
  });
34
46
  /** @readonly */
35
47
  readonly target_buffer_depth: number;
@@ -37,13 +49,17 @@ export class TimeDilation {
37
49
  readonly max_dilation: number;
38
50
  /** @readonly */
39
51
  readonly gain: number;
52
+ /** @readonly */
53
+ readonly max_round_trip_frames: number;
40
54
  /**
41
55
  * Compute the dilation factor for a client whose current input buffer depth
42
56
  * is `current_depth` (number of unconsumed input frames).
43
57
  *
44
58
  * @param {number} current_depth
45
- * @returns {number} factor in `[1 - max_dilation, 1 + max_dilation]`
59
+ * @returns {number} factor in `[1 - max_dilation, ∞)`; at most `1 + max_dilation` for a lead the
60
+ * steering repays
46
61
  */
47
62
  compute(current_depth: number): number;
63
+ #private;
48
64
  }
49
65
  //# sourceMappingURL=TimeDilation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"TimeDilation.d.ts","sourceRoot":"","sources":["../../../../../src/engine/network/time/TimeDilation.js"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;GAmBG;AACH;IAEI;;;;;;OAMG;IACH,0DANW;QACV,mBAAuB,CAAC,EAAE,MAAM,CAAC;QACjC,YAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,IAAQ,CAAC,EAAE,MAAM,CAAC;KACf,EAeH;IARG,gBAAgB;IAChB,qCAA8C;IAE9C,gBAAgB;IAChB,8BAAgC;IAEhC,gBAAgB;IAChB,sBAAgB;IAGpB;;;;;;OAMG;IACH,uBAHW,MAAM,GACJ,MAAM,CAQlB;CACJ"}
1
+ {"version":3,"file":"TimeDilation.d.ts","sourceRoot":"","sources":["../../../../../src/engine/network/time/TimeDilation.js"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH;IAQI;;;;;;;;;;;OAWG;IACH,iFAXW;QACV,mBAAuB,CAAC,EAAE,MAAM,CAAC;QACjC,YAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,IAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,qBAAyB,EAAE,MAAM,CAAC;KAC/B,EA6BH;IAbG,gBAAgB;IAChB,qCAA8C;IAE9C,gBAAgB;IAChB,8BAAgC;IAEhC,gBAAgB;IAChB,sBAAgB;IAEhB,gBAAgB;IAChB,uCAAkD;IAKtD;;;;;;;OAOG;IACH,uBAJW,MAAM,GACJ,MAAM,CAWlB;;CACJ"}
@@ -2,39 +2,61 @@ import { assert } from "../../../core/assert.js";
2
2
  import { clamp } from "../../../core/math/clamp.js";
3
3
 
4
4
  /**
5
- * Server-side computation of "tell client to run a bit faster or slower."
5
+ * How fast a client runs, from where its input lands at the host.
6
6
  *
7
- * The server maintains a target depth for its per-client input buffer. When the
8
- * buffer is too shallow (client's inputs aren't arriving fast enough), the
9
- * server tells the client to run the next few ticks at a slightly compressed
10
- * tick duration ("speed up"). When the buffer is too deep (client is sending
11
- * inputs faster than the server consumes them), the server asks for stretching.
12
- *
13
- * This is the same primitive used in Overwatch ("time dilation") and Rocket
14
- * League. Adjustment is bounded to a few percent so it's imperceptible.
15
- *
16
- * Output is a multiplier on tick duration:
7
+ * The host reports, each tick, how many frames ahead of its simulation the client's newest frame is:
8
+ * the depth of its input buffer for that client. The client wants it at {@link target_buffer_depth},
9
+ * deep enough that its input is there when the host needs it and no deeper, since every frame of it is
10
+ * a frame of latency. {@link compute} turns the depth into a multiplier on the client's tick duration:
17
11
  * factor < 1.0 → run faster (smaller tick interval)
18
12
  * factor > 1.0 → run slower (larger tick interval)
19
13
  * factor = 1.0 → run at nominal rate
20
14
  *
15
+ * Behind, and up to `max_dilation × T` frames ahead, an error is steered at up to {@link max_dilation}:
16
+ * a few percent, too little to see, which is what jitter and clock drift need. This is the primitive
17
+ * Overwatch and Rocket League call time dilation. A larger lead — the host lost time: a hidden or
18
+ * hitched host tab — is repaid in proportion to its size: the client runs at `1 / (1 + lead / T)` of
19
+ * its rate, which nears a standstill and never reaches it, so it keeps stepping and reading the host's
20
+ * reports. The reports are a round trip old, and a response in proportion to an error that old repays
21
+ * it without overshooting when its time constant is at least `e` times the delay, so `T` is
22
+ * `e × max_round_trip_frames` — 87 frames for a 32-frame round trip, which puts the steering's reach
23
+ * at 4.35 frames: on every round trip up to that one, a lead is repaid to the end the steering reaches
24
+ * on its own. Behind, the steering is all there is: running fast costs steps, and a client a whole
25
+ * window behind re-takes the host's time instead (`NetworkSession`).
26
+ *
21
27
  * @author Alex Goldring
22
28
  * @copyright Company Named Limited (c) 2026
23
29
  */
24
30
  export class TimeDilation {
25
31
 
32
+ /**
33
+ * The time constant, in frames, at which a lead is repaid: `e` of the longest round trips.
34
+ * @type {number}
35
+ */
36
+ #repay_frames;
37
+
26
38
  /**
27
39
  * @param {{
28
40
  * target_buffer_depth?: number,
29
41
  * max_dilation?: number,
30
42
  * gain?: number,
31
- * }} [options]
43
+ * max_round_trip_frames: number,
44
+ * }} options
45
+ *
46
+ * `max_round_trip_frames`: the longest round trip, in frames, the client runs on — how old, at
47
+ * most, the reports this works from are. Its owners pass its action log's depth: on a longer round
48
+ * trip the host's frames land past the log.
32
49
  */
33
- constructor({ target_buffer_depth = 2, max_dilation = 0.05, gain = 0.05 } = {}) {
50
+ constructor({ target_buffer_depth = 2, max_dilation = 0.05, gain = 0.05, max_round_trip_frames } = {}) {
34
51
  assert.isNumber(target_buffer_depth, 'target_buffer_depth');
35
52
  assert.isNumber(max_dilation, 'max_dilation');
36
53
  assert.isNumber(gain, 'gain');
37
54
 
55
+ // A real check: NaN here reaches the engine's fixed-step time scale and stops the client.
56
+ if (!(max_round_trip_frames > 0 && Number.isFinite(max_round_trip_frames))) {
57
+ throw new Error(`TimeDilation: max_round_trip_frames must be a positive number of frames, got ${max_round_trip_frames}`);
58
+ }
59
+
38
60
  /** @readonly */
39
61
  this.target_buffer_depth = target_buffer_depth;
40
62
 
@@ -43,6 +65,11 @@ export class TimeDilation {
43
65
 
44
66
  /** @readonly */
45
67
  this.gain = gain;
68
+
69
+ /** @readonly */
70
+ this.max_round_trip_frames = max_round_trip_frames;
71
+
72
+ this.#repay_frames = Math.E * max_round_trip_frames;
46
73
  }
47
74
 
48
75
  /**
@@ -50,13 +77,16 @@ export class TimeDilation {
50
77
  * is `current_depth` (number of unconsumed input frames).
51
78
  *
52
79
  * @param {number} current_depth
53
- * @returns {number} factor in `[1 - max_dilation, 1 + max_dilation]`
80
+ * @returns {number} factor in `[1 - max_dilation, ∞)`; at most `1 + max_dilation` for a lead the
81
+ * steering repays
54
82
  */
55
83
  compute(current_depth) {
56
84
  assert.isNumber(current_depth, 'current_depth');
85
+
57
86
  // depth > target ⇒ slow client down (factor > 1). depth < target ⇒ speed up (factor < 1).
58
- const raw = (current_depth - this.target_buffer_depth) * this.gain;
59
- const clamped = clamp(raw, -this.max_dilation, this.max_dilation);
60
- return 1.0 + clamped;
87
+ const error = current_depth - this.target_buffer_depth;
88
+ const steered = 1.0 + clamp(error * this.gain, -this.max_dilation, this.max_dilation);
89
+
90
+ return error > 0 ? Math.max(steered, 1.0 + error / this.#repay_frames) : steered;
61
91
  }
62
92
  }
@@ -66,11 +66,11 @@ export class FluidObstacleSystem extends System<any> {
66
66
  /**
67
67
  * @param {FluidComponent} fluid
68
68
  */
69
- static "__#485@#refresh_masks"(fluid: FluidComponent): void;
69
+ static "__#486@#refresh_masks"(fluid: FluidComponent): void;
70
70
  /**
71
71
  * @param {FluidComponent} fluid
72
72
  */
73
- static "__#485@#clear_field"(fluid: FluidComponent): void;
73
+ static "__#486@#clear_field"(fluid: FluidComponent): void;
74
74
  /**
75
75
  * Mark every cell of `fluid` whose centre lies within `inflation` of the
76
76
  * posed shape as solid. Iteration is clipped to the shape's world AABB
@@ -82,7 +82,7 @@ export class FluidObstacleSystem extends System<any> {
82
82
  * @param {number} inflation world-units SDF threshold
83
83
  * @param {Float64Array} point length-3 scratch
84
84
  */
85
- static "__#485@#voxelize"(fluid: FluidComponent, posed: PosedShape3D, aabb: Float64Array, inflation: number, point: Float64Array): void;
85
+ static "__#486@#voxelize"(fluid: FluidComponent, posed: PosedShape3D, aabb: Float64Array, inflation: number, point: Float64Array): void;
86
86
  /**
87
87
  * Write the obstacle's translation velocity onto every face of every cell
88
88
  * it voxelized — the moving-wall boundary condition. Runs AFTER the mask
@@ -102,7 +102,7 @@ export class FluidObstacleSystem extends System<any> {
102
102
  * @param {number} wvy
103
103
  * @param {number} wvz
104
104
  */
105
- static "__#485@#stamp_wall_velocity"(fluid: FluidComponent, posed: PosedShape3D, aabb: Float64Array, inflation: number, point: Float64Array, wvx: number, wvy: number, wvz: number): void;
105
+ static "__#486@#stamp_wall_velocity"(fluid: FluidComponent, posed: PosedShape3D, aabb: Float64Array, inflation: number, point: Float64Array, wvx: number, wvy: number, wvz: number): void;
106
106
  constructor();
107
107
  dependencies: (typeof FluidObstacle)[];
108
108
  components_used: (ResourceAccessSpecification<typeof Transform64> | ResourceAccessSpecification<typeof FluidObstacle> | ResourceAccessSpecification<typeof Collider> | ResourceAccessSpecification<typeof RigidBody> | ResourceAccessSpecification<typeof FluidComponent>)[];
@@ -3,7 +3,7 @@
3
3
  * wood, and thinner wood that thicker wood has grown into can be found. A unit is added once, when it has grown, under
4
4
  * the cells its axis line crosses; a query reads every unit's place and radius from its axis as they are then, and
5
5
  * reaches as far round as the thickest wood ({@link max_radius}), so wood that thickens needs no re-adding. The units
6
- * of an axis that has died are passed over.
6
+ * of an axis that has died are passed over, and leave the hash at the next {@link refresh}.
7
7
  */
8
8
  export class ShootWoodHash {
9
9
  /**
@@ -69,7 +69,8 @@ export class ShootWoodHash {
69
69
  add(m: number, k: number): void;
70
70
  /**
71
71
  * Records each cell's thickest living unit, after the wood has thickened, and the thickest of all
72
- * ({@link max_radius}).
72
+ * ({@link max_radius}). A dead axis does not live again, so its units leave their cells, in the order the rest
73
+ * were added, and a cell left empty leaves the hash.
73
74
  */
74
75
  refresh(): void;
75
76
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"ShootWoodHash.d.ts","sourceRoot":"","sources":["../../../../../src/generation/foliage/skeleton/ShootWoodHash.js"],"names":[],"mappings":"AAwBA;;;;;;GAMG;AACH;IA8FI;;;;;;OAMG;IACH,uBALW;QAAC,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAA;KAAC,KAClC,MAAM,KACN,MAAM,GACJ,MAAM,CAMlB;IApED;;OAEG;IACH,kBAFW,MAAM;QAAC,KAAK,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAA;KAAC,CAAC,EAIrF;IAzCD,oCAAoC;IACpC,OADW,IAAI,MAAM,EAAE,MAAM,EAAE,CAAC,CACd;IAElB;;;;OAIG;IACH,UAFU,IAAI,MAAM,EAAE,MAAM,CAAC,CAER;IAErB;;;OAGG;IACH,YAFU,MAAM,CAED;IAEf;;;OAGG;IACH,WAFU,MAAM,EAAE,CAEH;IAEf;;;OAGG;IACH,YAFU,MAAM,EAAE,CAEF;IAEhB;;;OAGG;IACH,MAFU,MAAM,EAAE,CAER;IAEV,cAAU;IAMN;eAHqB,OAAO;gBAAU,MAAM,EAAE;eAAS,MAAM,EAAE;eAAS,MAAM,EAAE;QAGhE;IAGpB;;;;OAIG;IACH,OAHW,MAAM,KACN,MAAM,QAsBhB;IAED;;;OAGG;IACH,gBAiBC;IAeD;;;;;;;;;;;OAWG;IACH,QAPW,UAAU,MAAM,CAAC,KACjB,UAAU,MAAM,CAAC,UACjB,MAAM,cACN,MAAM,eACG,MAAM,QAAE,MAAM,QAAE,MAAM,QAAE,MAAM,KAAG,OAAO,GAC/C,MAAM,CA6ClB;CACJ"}
1
+ {"version":3,"file":"ShootWoodHash.d.ts","sourceRoot":"","sources":["../../../../../src/generation/foliage/skeleton/ShootWoodHash.js"],"names":[],"mappings":"AAiCA;;;;;;GAMG;AACH;IA4GI;;;;;;OAMG;IACH,uBALW;QAAC,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAA;KAAC,KAClC,MAAM,KACN,MAAM,GACJ,MAAM,CAMlB;IAlFD;;OAEG;IACH,kBAFW,MAAM;QAAC,KAAK,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAA;KAAC,CAAC,EAIrF;IAzCD,oCAAoC;IACpC,OADW,IAAI,MAAM,EAAE,MAAM,EAAE,CAAC,CACd;IAElB;;;;OAIG;IACH,UAFU,IAAI,MAAM,EAAE,MAAM,CAAC,CAER;IAErB;;;OAGG;IACH,YAFU,MAAM,CAED;IAEf;;;OAGG;IACH,WAFU,MAAM,EAAE,CAEH;IAEf;;;OAGG;IACH,YAFU,MAAM,EAAE,CAEF;IAEhB;;;OAGG;IACH,MAFU,MAAM,EAAE,CAER;IAEV,cAAU;IAMN;eAHqB,OAAO;gBAAU,MAAM,EAAE;eAAS,MAAM,EAAE;eAAS,MAAM,EAAE;QAGhE;IAGpB;;;;OAIG;IACH,OAHW,MAAM,KACN,MAAM,QA2BhB;IAED;;;;OAIG;IACH,gBAyBC;IAeD;;;;;;;;;;;OAWG;IACH,QAPW,UAAU,MAAM,CAAC,KACjB,UAAU,MAAM,CAAC,UACjB,MAAM,cACN,MAAM,eACG,MAAM,QAAE,MAAM,QAAE,MAAM,QAAE,MAAM,KAAG,OAAO,GAC/C,MAAM,CA6ClB;CACJ"}
@@ -1,3 +1,4 @@
1
+ import { aabb3_intersects_segment3 } from "../../../core/geom/3d/aabb/aabb3_intersects_segment3.js";
1
2
  import { line3_compute_segment_segment_closest_points } from "../../../core/geom/3d/line/line3_compute_segment_segment_closest_points.js";
2
3
  import { shoot_radius } from "./shoot_geometry.js";
3
4
 
@@ -20,6 +21,14 @@ const CLEARANCE = 0.001;
20
21
  */
21
22
  const KEY_SPAN = 2048;
22
23
 
24
+ /**
25
+ * Margin (m) a cell is widened by when asking whether an axis line crosses it: far above the rounding of a point at
26
+ * the key's 205 m span, far below any wood, so a line along a cell's face or through its edge is under the cells either
27
+ * side of it.
28
+ * @type {number}
29
+ */
30
+ const CROSSING_TOLERANCE = WOOD_CELL * 2 ** -20;
31
+
23
32
  const SCRATCH_ST = new Float64Array(2);
24
33
 
25
34
  /**
@@ -27,7 +36,7 @@ const SCRATCH_ST = new Float64Array(2);
27
36
  * wood, and thinner wood that thicker wood has grown into can be found. A unit is added once, when it has grown, under
28
37
  * the cells its axis line crosses; a query reads every unit's place and radius from its axis as they are then, and
29
38
  * reaches as far round as the thickest wood ({@link max_radius}), so wood that thickens needs no re-adding. The units
30
- * of an axis that has died are passed over.
39
+ * of an axis that has died are passed over, and leave the hash at the next {@link refresh}.
31
40
  */
32
41
  export class ShootWoodHash {
33
42
  /** @type {Map<number, number[]>} */
@@ -89,6 +98,11 @@ export class ShootWoodHash {
89
98
  const y0 = Math.floor(Math.min(p[a + 1], p[b + 1]) / WOOD_CELL), y1 = Math.floor(Math.max(p[a + 1], p[b + 1]) / WOOD_CELL);
90
99
  const z0 = Math.floor(Math.min(p[a + 2], p[b + 2]) / WOOD_CELL), z1 = Math.floor(Math.max(p[a + 2], p[b + 2]) / WOOD_CELL);
91
100
  for (let x = x0; x <= x1; x++) for (let y = y0; y <= y1; y++) for (let z = z0; z <= z1; z++) {
101
+ if (!aabb3_intersects_segment3(
102
+ x * WOOD_CELL - CROSSING_TOLERANCE, y * WOOD_CELL - CROSSING_TOLERANCE, z * WOOD_CELL - CROSSING_TOLERANCE,
103
+ (x + 1) * WOOD_CELL + CROSSING_TOLERANCE, (y + 1) * WOOD_CELL + CROSSING_TOLERANCE, (z + 1) * WOOD_CELL + CROSSING_TOLERANCE,
104
+ p[a], p[a + 1], p[a + 2], p[b], p[b + 1], p[b + 2]
105
+ )) continue;
92
106
  const key = ((x + KEY_SPAN / 2) * KEY_SPAN + (y + KEY_SPAN / 2)) * KEY_SPAN + (z + KEY_SPAN / 2);
93
107
  let list = this.cells.get(key);
94
108
  if (list === undefined) {
@@ -102,21 +116,30 @@ export class ShootWoodHash {
102
116
 
103
117
  /**
104
118
  * Records each cell's thickest living unit, after the wood has thickened, and the thickest of all
105
- * ({@link max_radius}).
119
+ * ({@link max_radius}). A dead axis does not live again, so its units leave their cells, in the order the rest
120
+ * were added, and a cell left empty leaves the hash.
106
121
  */
107
122
  refresh() {
108
123
  const axes = this.axes;
109
124
  let most = 0;
110
125
  for (const [key, list] of this.cells) {
111
126
  let max = 0;
127
+ let kept = 0;
112
128
  for (let i = 0; i < list.length; i++) {
113
129
  const id = list[i];
114
130
  const axis = axes[this.unit_axis[id]];
115
131
  if (!axis.alive) continue;
132
+ list[kept++] = id;
116
133
  const k = this.unit_index[id];
117
134
  const thickest = axis.radii.length > k + 1 ? Math.max(axis.radii[k], axis.radii[k + 1]) : shoot_radius(axis.units[k], 0);
118
135
  if (thickest > max) max = thickest;
119
136
  }
137
+ if (kept === 0) {
138
+ this.cells.delete(key);
139
+ this.cell_max.delete(key);
140
+ continue;
141
+ }
142
+ list.length = kept;
120
143
  this.cell_max.set(key, max);
121
144
  if (max > most) most = max;
122
145
  }
@@ -5,7 +5,9 @@
5
5
  * @param {number} buffer
6
6
  * @param {number} [offset=0]
7
7
  * @param {number} [size] defaults to buffer.size - offset
8
- * @param {number} [timeout] in milliseconds, 0 for no timeout. Timeout will start immediately, meaning that if you don't schedule the graph immediately - you might see unexpected behaviour.
8
+ * @param {number} [timeout] in milliseconds, 0 for no timeout: how long the read waits for its pass to run, starting
9
+ * now. A graph that is never run, or stops at a throwing pass before this one, rejects the read here. Once the pass has
10
+ * run, the read waits for the device however far behind it is, and settles when the mapping does.
9
11
  * @returns {Promise<Reference<ArrayBuffer>>} don't forget to release the reference
10
12
  */
11
13
  export function graph_read_buffer({ graph, buffer, offset, size, timeout }: FrameGraph): Promise<Reference<ArrayBuffer>>;
@@ -1 +1 @@
1
- {"version":3,"file":"graph_read_buffer.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/buffer/graph_read_buffer.js"],"names":[],"mappings":"AAOA;;;;;;;;;GASG;AACH,yFAFa,QAAQ,UAAU,WAAW,CAAC,CAAC,CAuH3C;0BApIyB,2CAA2C"}
1
+ {"version":3,"file":"graph_read_buffer.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/buffer/graph_read_buffer.js"],"names":[],"mappings":"AAOA;;;;;;;;;;;GAWG;AACH,yFAFa,QAAQ,UAAU,WAAW,CAAC,CAAC,CAwH3C;0BAvIyB,2CAA2C"}
@@ -12,7 +12,9 @@ const scratch_buffer_descriptor = new BufferResourceDescriptor();
12
12
  * @param {number} buffer
13
13
  * @param {number} [offset=0]
14
14
  * @param {number} [size] defaults to buffer.size - offset
15
- * @param {number} [timeout] in milliseconds, 0 for no timeout. Timeout will start immediately, meaning that if you don't schedule the graph immediately - you might see unexpected behaviour.
15
+ * @param {number} [timeout] in milliseconds, 0 for no timeout: how long the read waits for its pass to run, starting
16
+ * now. A graph that is never run, or stops at a throwing pass before this one, rejects the read here. Once the pass has
17
+ * run, the read waits for the device however far behind it is, and settles when the mapping does.
16
18
  * @returns {Promise<Reference<ArrayBuffer>>} don't forget to release the reference
17
19
  */
18
20
  export function graph_read_buffer({
@@ -47,6 +49,12 @@ export function graph_read_buffer({
47
49
  * @param {GraphicsExecutionContext} context
48
50
  */
49
51
  function execute(data, resources, context) {
52
+ // the copy is recorded: the mapping lands, or is refused, however long the device takes
53
+ if (timeout_handle !== -1) {
54
+ clearTimeout(timeout_handle);
55
+ timeout_handle = -1;
56
+ }
57
+
50
58
  /**
51
59
  * @type {GPUBuffer}
52
60
  */
@@ -94,11 +102,6 @@ export function graph_read_buffer({
94
102
  return;
95
103
  }
96
104
 
97
- if (timeout_handle !== -1) {
98
- clearTimeout(timeout_handle);
99
- timeout_handle = -1;
100
- }
101
-
102
105
  const data = destination.getMappedRange(0, _size);
103
106
 
104
107
  ref = new Reference();
@@ -1,6 +1,11 @@
1
1
  /**
2
2
  * Shader is useful for copying buffers where the buffers are missing `GPUBufferUsage.COPY_DST` or `GPUBufferUsage.COPY_SRC`.
3
3
  * This is a pure utility shader.
4
+ *
5
+ * Copies the first `size` words. The launch may be smaller than that — a copy longer than
6
+ * `maxComputeWorkgroupsPerDimension` workgroups has to be — and every lane strides through the rest
7
+ * by the whole grid, so a caller that clamps the launch still has every word copied.
8
+ *
4
9
  * @type {ComputeShader}
5
10
  */
6
11
  export const shader_buffer_copy_storage_to_storage: ComputeShader;
@@ -1 +1 @@
1
- {"version":3,"file":"shader_buffer_copy_storage_to_storage.d.ts","sourceRoot":"","sources":["../../../../../../src/shade/renderer/buffer/util/shader_buffer_copy_storage_to_storage.js"],"names":[],"mappings":"AAkCA;;;;GAIG;AACH,oDAFU,aAAa,CAMpB;8BAvC2B,+BAA+B"}
1
+ {"version":3,"file":"shader_buffer_copy_storage_to_storage.d.ts","sourceRoot":"","sources":["../../../../../../src/shade/renderer/buffer/util/shader_buffer_copy_storage_to_storage.js"],"names":[],"mappings":"AAkCA;;;;;;;;;GASG;AACH,oDAFU,aAAa,CAMpB;8BA5C2B,+BAA+B"}