@woosh/meep-engine 2.72.0 → 2.74.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/build/bundle-worker-image-decoder.js +1 -1
  2. package/build/meep.cjs +38 -189
  3. package/build/meep.min.js +1 -1
  4. package/build/meep.module.js +38 -189
  5. package/package.json +3 -2
  6. package/src/engine/asset/loaders/image/codec/NativeImageDecoder.js +2 -1
  7. package/src/engine/asset/loaders/image/codec/ThreadedImageDecoder.js +4 -5
  8. package/src/engine/asset/loaders/image/png/PNGReader.js +23 -19
  9. package/src/engine/graphics/ecs/camera/FrustumProjector.js +31 -182
  10. package/src/engine/graphics/generate_halton_jitter.js +21 -0
  11. package/src/engine/graphics/render/buffer/simple-fx/taa/TemporalSupersamplingRenderPlugin.js +3 -20
  12. package/src/engine/graphics/texture/virtual/v2/NOTES.md +11 -1
  13. package/src/engine/graphics/texture/virtual/v2/ShaderUsage.js +7 -5
  14. package/src/engine/graphics/texture/virtual/v2/SparseTexture.js +291 -0
  15. package/src/engine/graphics/texture/virtual/v2/TileLoader.js +232 -0
  16. package/src/engine/graphics/texture/virtual/v2/UsageMetadata.js +118 -134
  17. package/src/engine/graphics/texture/virtual/v2/VirtualTextureManager.js +73 -9
  18. package/src/engine/graphics/texture/virtual/v2/debug/ResidencyDebugView.js +58 -0
  19. package/src/engine/graphics/texture/virtual/v2/debug/UsageDebugView.js +63 -0
  20. package/src/engine/graphics/texture/virtual/v2/{UsagePyramidDebugView.js → debug/UsagePyramidDebugView.js} +27 -23
  21. package/src/engine/graphics/texture/virtual/v2/prototype.js +62 -19
  22. package/src/engine/graphics/texture/virtual/v2/tile/TextureTile.js +31 -0
  23. package/src/engine/graphics/texture/virtual/v2/tile/compose_finger_print.js +23 -0
  24. package/src/engine/graphics/texture/virtual/v2/tile/compose_tile_address.js +22 -0
  25. package/src/engine/graphics/texture/virtual/v2/tile/decompose_finger_print.js +12 -0
  26. package/src/engine/graphics/texture/virtual/v2/tile/finger_print_to_tile_address.js +16 -0
  27. package/src/engine/graphics/texture/virtual/v2/tile/tile_address_to_finger_print.js +35 -0
  28. package/src/view/CSS_ABSOLUTE_POSITIONING.js +5 -0
  29. package/src/engine/graphics/clouds/MaterialTransformer.js +0 -0
  30. package/src/engine/graphics/clouds/TerrainCloudsPlugin.js +0 -0
  31. package/src/engine/graphics/clouds/cs_build_fragment_shader.js +0 -0
  32. package/src/engine/graphics/clouds/cs_build_vertex_shader.js +0 -0
  33. package/src/engine/graphics/ecs/camera/TiltCameraController.js +0 -69
  34. package/src/engine/graphics/ecs/camera/is_valid_distance_value.js +0 -11
  35. package/src/engine/graphics/texture/virtual/v2/UsageDebugView.js +0 -51
@@ -6,190 +6,41 @@
6
6
  import { Vector3 as ThreeVector3 } from 'three';
7
7
 
8
8
 
9
- const projectInWorldSpace = (function () {
10
- const corners = [
11
- new ThreeVector3(-1, -1, 0),
12
- new ThreeVector3(-1, -1, 0),
9
+ const corners = [
10
+ new ThreeVector3(-1, -1, 0),
11
+ new ThreeVector3(-1, -1, 0),
13
12
 
14
- new ThreeVector3(-1, 1, 0),
15
- new ThreeVector3(-1, 1, 0),
13
+ new ThreeVector3(-1, 1, 0),
14
+ new ThreeVector3(-1, 1, 0),
16
15
 
17
- new ThreeVector3(1, -1, 0),
18
- new ThreeVector3(1, -1, 0),
16
+ new ThreeVector3(1, -1, 0),
17
+ new ThreeVector3(1, -1, 0),
19
18
 
20
- new ThreeVector3(1, 1, 0),
21
- new ThreeVector3(1, 1, 0)
22
- ];
19
+ new ThreeVector3(1, 1, 0),
20
+ new ThreeVector3(1, 1, 0)
21
+ ];
23
22
 
24
23
 
25
- /**
26
- *
27
- * @param {Number} nearZ
28
- * @param {Number} farZ
29
- * @param {THREE.Camera} camera
30
- * @param {THREE.Matrix4} matrixWorldInverse
31
- * @param {function} callback
32
- */
33
- function projectInWorldSpace(nearZ, farZ, camera, callback) {
34
- corners[0].set(-1, -1, nearZ);
35
- corners[1].set(-1, -1, farZ);
36
-
37
- corners[2].set(-1, 1, nearZ);
38
- corners[3].set(-1, 1, farZ);
39
-
40
- corners[4].set(1, -1, nearZ);
41
- corners[5].set(1, -1, farZ);
42
-
43
- corners[6].set(1, 1, nearZ);
44
- corners[7].set(1, 1, farZ);
45
-
46
- //compute corners of view frustum in light space
47
- let x0 = Number.POSITIVE_INFINITY;
48
- let y0 = Number.POSITIVE_INFINITY;
49
- let z0 = Number.POSITIVE_INFINITY;
50
-
51
- let x1 = Number.NEGATIVE_INFINITY;
52
- let y1 = Number.NEGATIVE_INFINITY;
53
- let z1 = Number.NEGATIVE_INFINITY;
54
-
55
-
56
- for (let i = 0; i < 8; i++) {
57
- const corner = corners[i];
58
- corner.unproject(camera);
59
-
60
- if (corner.x < x0) {
61
- x0 = corner.x;
62
- }
63
- if (corner.x > x1) {
64
- x1 = corner.x;
65
- }
66
-
67
- if (corner.y < y0) {
68
- y0 = corner.y;
69
- }
70
- if (corner.y > y1) {
71
- y1 = corner.y;
72
- }
73
-
74
- if (corner.z < z0) {
75
- z0 = corner.z;
76
- }
77
- if (corner.z > z1) {
78
- z1 = corner.z;
79
- }
80
- }
81
-
82
- callback(x0, y0, z0, x1, y1, z1);
83
- }
84
-
85
- return projectInWorldSpace;
86
- })();
87
-
88
- const project = (function () {
89
- const corners = [
90
- new ThreeVector3(-1, -1, 0),
91
- new ThreeVector3(-1, -1, 0),
92
-
93
- new ThreeVector3(-1, 1, 0),
94
- new ThreeVector3(-1, 1, 0),
95
-
96
- new ThreeVector3(1, -1, 0),
97
- new ThreeVector3(1, -1, 0),
98
-
99
- new ThreeVector3(1, 1, 0),
100
- new ThreeVector3(1, 1, 0)
101
- ];
102
-
103
-
104
- /**
105
- *
106
- * @param {Number} nearZ
107
- * @param {Number} farZ
108
- * @param {THREE.Camera} camera
109
- * @param {THREE.Matrix4} matrixWorldInverse
110
- * @param {AABB3} result
111
- */
112
- function project(nearZ, farZ, camera, matrixWorldInverse, result) {
113
- corners[0].set(-1, -1, nearZ);
114
- corners[1].set(-1, -1, farZ);
115
-
116
- corners[2].set(-1, 1, nearZ);
117
- corners[3].set(-1, 1, farZ);
118
-
119
- corners[4].set(1, -1, nearZ);
120
- corners[5].set(1, -1, farZ);
121
-
122
- corners[6].set(1, 1, nearZ);
123
- corners[7].set(1, 1, farZ);
124
-
125
- //compute corners of view frustum in light space
126
- let x0 = Number.POSITIVE_INFINITY;
127
- let y0 = Number.POSITIVE_INFINITY;
128
- let z0 = Number.POSITIVE_INFINITY;
129
-
130
- let x1 = Number.NEGATIVE_INFINITY;
131
- let y1 = Number.NEGATIVE_INFINITY;
132
- let z1 = Number.NEGATIVE_INFINITY;
133
-
134
-
135
- for (let i = 0; i < 8; i++) {
136
- const corner = corners[i];
137
- corner.unproject(camera);
138
- corner.applyMatrix4(matrixWorldInverse);
139
-
140
- if (corner.x < x0) {
141
- x0 = corner.x;
142
- }
143
- if (corner.x > x1) {
144
- x1 = corner.x;
145
- }
146
-
147
- if (corner.y < y0) {
148
- y0 = corner.y;
149
- }
150
- if (corner.y > y1) {
151
- y1 = corner.y;
152
- }
153
-
154
- if (corner.z < z0) {
155
- z0 = corner.z;
156
- }
157
- if (corner.z > z1) {
158
- z1 = corner.z;
159
- }
160
- }
161
-
162
- result.setBounds(x0, y0, z0, x1, y1, z1);
163
- }
164
-
165
- return project;
166
- })();
167
-
168
24
  /**
169
25
  *
170
- * @param _x0
171
- * @param _y0
172
- * @param _z0
173
- * @param _x1
174
- * @param _y1
175
- * @param _z1
176
- * @param {Camera} camera
177
- * @param callback
26
+ * @param {Number} nearZ
27
+ * @param {Number} farZ
28
+ * @param {THREE.Camera} camera
29
+ * @param {THREE.Matrix4} matrixWorldInverse
30
+ * @param {AABB3} result
178
31
  */
179
- function unproject(_x0, _y0, _z0, _x1, _y1, _z1, camera, callback) {
180
- const corners = [
181
- new ThreeVector3(_x0, _y0, _z0),
182
- new ThreeVector3(_x0, _y0, _z1),
32
+ function project(nearZ, farZ, camera, matrixWorldInverse, result) {
33
+ corners[0].set(-1, -1, nearZ);
34
+ corners[1].set(-1, -1, farZ);
183
35
 
184
- new ThreeVector3(_x0, _y1, _z0),
185
- new ThreeVector3(_x0, _y1, _z1),
36
+ corners[2].set(-1, 1, nearZ);
37
+ corners[3].set(-1, 1, farZ);
186
38
 
187
- new ThreeVector3(_x1, _y0, _z0),
188
- new ThreeVector3(_x1, _y0, _z1),
39
+ corners[4].set(1, -1, nearZ);
40
+ corners[5].set(1, -1, farZ);
189
41
 
190
- new ThreeVector3(_x1, _y1, _z0),
191
- new ThreeVector3(_x1, _y1, _z1)
192
- ];
42
+ corners[6].set(1, 1, nearZ);
43
+ corners[7].set(1, 1, farZ);
193
44
 
194
45
  //compute corners of view frustum in light space
195
46
  let x0 = Number.POSITIVE_INFINITY;
@@ -201,8 +52,10 @@ function unproject(_x0, _y0, _z0, _x1, _y1, _z1, camera, callback) {
201
52
  let z1 = Number.NEGATIVE_INFINITY;
202
53
 
203
54
 
204
- corners.forEach(function (corner) {
205
- corner.project(camera);
55
+ for (let i = 0; i < 8; i++) {
56
+ const corner = corners[i];
57
+ corner.unproject(camera);
58
+ corner.applyMatrix4(matrixWorldInverse);
206
59
 
207
60
  if (corner.x < x0) {
208
61
  x0 = corner.x;
@@ -224,19 +77,15 @@ function unproject(_x0, _y0, _z0, _x1, _y1, _z1, camera, callback) {
224
77
  if (corner.z > z1) {
225
78
  z1 = corner.z;
226
79
  }
227
- });
80
+ }
228
81
 
229
- callback(x0, y0, z0, x1, y1, z1);
82
+ result.setBounds(x0, y0, z0, x1, y1, z1);
230
83
  }
231
84
 
85
+
232
86
  export default {
233
87
  /**
234
88
  * @deprecated use non-threejs specific code
235
89
  */
236
90
  project,
237
- unproject,
238
- /**
239
- * @deprecated use non-threejs specific code
240
- */
241
- projectInWorldSpace
242
91
  };
@@ -0,0 +1,21 @@
1
+ import { assert } from "../../core/assert.js";
2
+ import { halton_sequence } from "../../core/math/statistics/halton_sequence.js";
3
+
4
+ /**
5
+ * Generates a number of 2d jitter offsets in range -1...1
6
+ * @param {number} point_count
7
+ * @return {Float32Array}
8
+ */
9
+ export function generate_halton_jitter(point_count) {
10
+ assert.isNonNegativeInteger(point_count, 'point_count');
11
+
12
+ const result = new Float32Array(point_count * 2);
13
+
14
+ for (let i = 0; i < point_count; i++) {
15
+ const i2 = i * 2;
16
+ result[i2] = halton_sequence(2, i) * 2 - 1;
17
+ result[i2 + 1] = halton_sequence(3, i) * 2 - 1;
18
+ }
19
+
20
+ return result;
21
+ }
@@ -1,23 +1,6 @@
1
- import { EnginePlugin } from "../../../../../plugin/EnginePlugin.js";
2
- import { halton_sequence } from "../../../../../../core/math/statistics/halton_sequence.js";
3
1
  import { array_copy } from "../../../../../../core/collection/array/array_copy.js";
4
-
5
- /**
6
- * Generates a number of 2d jitter offsets in range -1...1
7
- * @param {number} point_count
8
- * @return {Float32Array}
9
- */
10
- function generate_jitter(point_count) {
11
- const result = new Float32Array(point_count * 2);
12
-
13
- for (let i = 0; i < point_count; i++) {
14
- const i2 = i * 2;
15
- result[i2] = halton_sequence(2, i) * 2 - 1;
16
- result[i2 + 1] = halton_sequence(3, i) * 2 - 1;
17
- }
18
-
19
- return result;
20
- }
2
+ import { EnginePlugin } from "../../../../../plugin/EnginePlugin.js";
3
+ import { generate_halton_jitter } from "../../../../generate_halton_jitter.js";
21
4
 
22
5
  export class TemporalSupersamplingRenderPlugin extends EnginePlugin {
23
6
  constructor() {
@@ -31,7 +14,7 @@ export class TemporalSupersamplingRenderPlugin extends EnginePlugin {
31
14
  this.__frame_index = 0;
32
15
 
33
16
  this.__sample_count = 16;
34
- this.__jitter_offset = generate_jitter(this.__sample_count);
17
+ this.__jitter_offset = generate_halton_jitter(this.__sample_count);
35
18
 
36
19
 
37
20
  this.__projection_modifier = {
@@ -14,4 +14,14 @@ The original texture is mip-mapped, down to the size of a single tile, for the s
14
14
 
15
15
  This is achieved by pre-rendering the scene with a special shader that records UV and mip-level information, which we call `Usage`, this is then analysed to build a list of used tiles (mip level, x, y) and by counting occurrence of this tile in the `Usage` texture we get a much more useful data structure.
16
16
 
17
- Based on the `Usage` data, we populate our `Physical` texture of pages, and based on what's currently in the `Physical` texture - we populate the `Reference` texture that stores information about the entire mip pyramid, and for each tile contains a pointer to the `Physical` texture, where representative tile can be found.
17
+ Based on the `Usage` data, we populate our `Physical` texture of pages, and based on what's currently in the `Physical` texture - we populate the `Reference` texture that stores information about the entire mip pyramid, and for each tile contains a pointer to the `Physical` texture, where representative tile can be found.
18
+
19
+
20
+ ## Considerations
21
+
22
+
23
+ ### Filtering
24
+
25
+ Most papers suggest introducing a border into each tile of ~4 pixels.
26
+ 4 pixel border on a 128x128 pixel tile is going to take up 12.1% of space, which is quite a lot.
27
+ We can do filtering in software, that is - sample individual pixels and perform interpolation inside the shader.
@@ -34,13 +34,15 @@ function fragment() {
34
34
 
35
35
  float MTCalculateMipLevel( const in vec2 uv, const in float bias ) {
36
36
 
37
- float scale_x = float(u_mt_tex.x) / u_mt_viewport_size.x;
38
- float scale_y = float(u_mt_tex.y) / u_mt_viewport_size.y;
37
+ float scale_x = float(u_mt_tex.x) ;
38
+ float scale_y = float(u_mt_tex.y) ;
39
39
 
40
- vec2 dx = dFdx( uv * scale_x);
41
- vec2 dy = dFdy( uv * scale_y);
40
+ vec2 dx = dFdx( uv * scale_x)/ u_mt_viewport_size.x;
41
+ vec2 dy = dFdy( uv * scale_y)/ u_mt_viewport_size.y;
42
42
 
43
- return log2( max( dot( dx, dx ), dot( dy, dy ) ) ) * 0.5 + bias;
43
+ float d = max( dot( dx, dx ), dot( dy, dy ) );
44
+
45
+ return log2( d ) * 0.5 + bias;
44
46
  }
45
47
 
46
48
  uvec4 MTCalculateTileKeyRGBA( const in vec2 uv ) {
@@ -0,0 +1,291 @@
1
+ import { WebGLRenderTarget } from "three";
2
+ import { BitSet } from "../../../../../core/binary/BitSet.js";
3
+ import { Cache } from "../../../../../core/cache/Cache.js";
4
+ import { array_copy } from "../../../../../core/collection/array/array_copy.js";
5
+ import { passThrough, strictEquals } from "../../../../../core/function/Functions.js";
6
+ import { max2 } from "../../../../../core/math/max2.js";
7
+ import { min2 } from "../../../../../core/math/min2.js";
8
+ import { tile_address_to_finger_print } from "./tile/tile_address_to_finger_print.js";
9
+ import { TileLoader } from "./TileLoader.js";
10
+
11
+ /**
12
+ * How much extra data to store in cache (in bytes)
13
+ * @type {number}
14
+ */
15
+ const DEFAULT_CACHE_SIZE = 16 * 1024 * 1024;
16
+
17
+ export class SparseTexture {
18
+ #page_texture = new WebGLRenderTarget(1, 1);
19
+
20
+ #page_texture_size = [1, 1];
21
+
22
+ #version = 0;
23
+
24
+ /**
25
+ * Maximum number of new page assignments per single update cycle
26
+ * This helps prevent thrashing and keep performance overhead of updating pages predictable
27
+ * @type {number}
28
+ */
29
+ #cycle_assignment_limit = 2;
30
+
31
+
32
+ get page_texture_resolution() {
33
+ return this.#page_texture_size;
34
+ }
35
+
36
+ /**
37
+ *
38
+ * @type {TextureTile[]}
39
+ */
40
+ #resident_tiles = [];
41
+
42
+ get resident_tiles() {
43
+ return this.#resident_tiles;
44
+ }
45
+
46
+ #page_slot_occupancy = new BitSet();
47
+
48
+ #tile_resolution = 128;
49
+
50
+ get tile_resolution() {
51
+ return this.#tile_resolution;
52
+ }
53
+
54
+ #residency_tile_capacity = 0;
55
+
56
+ /**
57
+ * Must be >=0
58
+ * When 0 - tiles that would not fit onto the page will not be loaded
59
+ * When >0 - same fraction of extra tiles will be fetching and put into cache, resulting in prefetching
60
+ * @type {number}
61
+ */
62
+ #prefetch_factor = 0.33;
63
+
64
+ #loader = new TileLoader();
65
+
66
+ /**
67
+ *
68
+ * @type {AssetManager|null}
69
+ */
70
+ #asset_manager = null;
71
+
72
+ constructor() {
73
+ this.#loader.on.loaded.add(this.#handle_tile_loaded, this);
74
+ }
75
+
76
+ /**
77
+ *
78
+ * @param {TextureTile} tile
79
+ */
80
+ #handle_tile_loaded(tile) {
81
+ const finderPrint = tile.finder_print;
82
+
83
+ if (this.#tile_cache.has(finderPrint)) {
84
+ // already loaded
85
+ return;
86
+ }
87
+
88
+ this.#tile_cache.put(finderPrint, tile);
89
+
90
+ }
91
+
92
+ /**
93
+ *
94
+ * @param {string} v
95
+ */
96
+ set path(v) {
97
+ this.#loader.path = v;
98
+ }
99
+
100
+ set asset_manager(v) {
101
+ this.#asset_manager = v
102
+
103
+ this.#loader.asset_manager = v;
104
+ }
105
+
106
+ #update_residency_capacity() {
107
+ const tile_resolution = this.#tile_resolution;
108
+ const size = this.#page_texture_size;
109
+
110
+ const x = size[0] / tile_resolution;
111
+ const y = size[1] / tile_resolution;
112
+
113
+ this.#residency_tile_capacity = x * y;
114
+
115
+ this.#loader.queue_limit = max2(16, this.#residency_tile_capacity);
116
+ }
117
+
118
+
119
+ /**
120
+ *
121
+ * @param {number} v
122
+ */
123
+ set tile_resolution(v) {
124
+ this.#tile_resolution = v;
125
+
126
+ this.#update_residency_capacity();
127
+ }
128
+
129
+ /**
130
+ * Tiles that are not currently resident live here
131
+ * key is the tile's "fingerprint"
132
+ * @type {Cache<number,TextureTile>}
133
+ */
134
+ #tile_cache = new Cache({
135
+ keyHashFunction: passThrough,
136
+ keyEqualityFunction: strictEquals,
137
+ maxWeight: DEFAULT_CACHE_SIZE,
138
+ valueWeigher: () => this.#tile_resolution * this.#tile_resolution * 4
139
+ });
140
+
141
+ #find_eviction_target_index() {
142
+ // find least-recently used
143
+ const tiles = this.#resident_tiles;
144
+ const count = tiles.length;
145
+
146
+ let min = Infinity;
147
+ let min_index = -1;
148
+
149
+ for (let i = 0; i < count; i++) {
150
+ const tile = tiles[i];
151
+
152
+ if (tile.last_used_time < min) {
153
+ min = tile.last_used_time;
154
+ min_index = i;
155
+ }
156
+ }
157
+
158
+ return min_index;
159
+ }
160
+
161
+ #evict_one() {
162
+ const index = this.#find_eviction_target_index();
163
+
164
+ if (index === -1) {
165
+ return false;
166
+ }
167
+
168
+ return this.#remove_resident_tile(index);
169
+ }
170
+
171
+ /**
172
+ *
173
+ * @param {number} index
174
+ * @returns {boolean}
175
+ */
176
+ #remove_resident_tile(index) {
177
+ const [removed] = this.#resident_tiles.splice(index, 1);
178
+
179
+ const pageSlot = removed.page_slot;
180
+
181
+ // mark slot as empty for next tile
182
+ this.#page_slot_occupancy.clear(pageSlot);
183
+
184
+ removed.page_slot = -1;
185
+
186
+ // push into cache
187
+ this.#tile_cache.put(removed.finder_print, removed);
188
+
189
+ return true;
190
+ }
191
+
192
+ /**
193
+ *
194
+ * @param {TextureTile} tile
195
+ */
196
+ #make_resident(tile) {
197
+
198
+ if (this.#residency_tile_capacity === 0) {
199
+ throw new Error('Capacity is 0');
200
+ }
201
+
202
+ let slot = this.#page_slot_occupancy.nextClearBit(0);
203
+
204
+ while (slot >= this.#residency_tile_capacity) {
205
+ this.#evict_one();
206
+
207
+ slot = this.#page_slot_occupancy.nextClearBit(0);
208
+ }
209
+
210
+ tile.page_slot = slot;
211
+
212
+ this.#page_slot_occupancy.set(slot, true);
213
+ this.#resident_tiles.push(tile);
214
+
215
+ return true;
216
+ }
217
+
218
+ /**
219
+ *
220
+ * @param {number[]} v
221
+ */
222
+ set page_texture_size(v) {
223
+ array_copy(
224
+ v, 0,
225
+ this.#page_texture_size, 0,
226
+ 2
227
+ );
228
+
229
+ this.#page_texture.dispose();
230
+ this.#page_texture.setSize(this.#page_texture_size[0], this.#page_texture_size[1]);
231
+
232
+ this.#update_residency_capacity();
233
+ }
234
+
235
+ /**
236
+ *
237
+ * @param {UsageMetadata} usage
238
+ */
239
+ update_usage(usage) {
240
+ const usage_occupancy = usage.occupancy;
241
+ const usage_occupancy_count = usage.occupancy_count;
242
+
243
+
244
+ // Usage occupancy is already sorted by priority, so there's no point is trying to fetch more than t
245
+ let fetch_limit = this.#residency_tile_capacity * (1 + this.#prefetch_factor);
246
+
247
+ let writes_remaining = min2(
248
+ max2(1, this.#cycle_assignment_limit),
249
+ this.#residency_tile_capacity
250
+ );
251
+
252
+ let remaining_slots = this.#residency_tile_capacity;
253
+
254
+ for (let i = 0; i < usage_occupancy_count && fetch_limit > 0; i++) {
255
+ const tile_address = usage_occupancy[i];
256
+
257
+ const fingerPrint = tile_address_to_finger_print(tile_address);
258
+
259
+ const tile = this.#tile_cache.get(fingerPrint);
260
+ if (tile === null) {
261
+ this.#loader.enqueue(fingerPrint);
262
+ } else if (remaining_slots > 0) {
263
+ // found in cache, and we have some slots remaining to write into
264
+
265
+
266
+ // touch to prevent eviction
267
+ tile.last_used_time = this.#version;
268
+
269
+ if (tile.page_slot === -1 && writes_remaining > 0) {
270
+ // tile is not active, and we can write it
271
+ writes_remaining--;
272
+ this.#make_resident(tile);
273
+ }
274
+
275
+ fetch_limit--;
276
+ remaining_slots--;
277
+ }
278
+ }
279
+
280
+
281
+ this.#loader.update_usage(usage);
282
+
283
+ this.#version++;
284
+ }
285
+
286
+ dispose() {
287
+ this.#page_texture.dispose();
288
+ this.#tile_cache.clear();
289
+ this.#page_slot_occupancy.reset();
290
+ }
291
+ }