@woosh/meep-engine 2.121.13 → 2.122.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/package.json +1 -1
- package/src/core/binary/allocator/OffsetAllocator.d.ts +113 -0
- package/src/core/binary/allocator/OffsetAllocator.d.ts.map +1 -0
- package/src/core/binary/allocator/OffsetAllocator.js +576 -0
- package/src/core/binary/float/SmallFloat.d.ts +20 -0
- package/src/core/binary/float/SmallFloat.d.ts.map +1 -0
- package/src/core/binary/float/SmallFloat.js +84 -0
- package/src/core/bvh2/bvh3/ebvh_build_for_geometry_morton.d.ts.map +1 -1
- package/src/core/bvh2/bvh3/ebvh_build_for_geometry_morton.js +15 -0
- package/src/core/bvh2/bvh3/ebvh_nodes_sort_sah_local4.d.ts.map +1 -1
- package/src/core/bvh2/bvh3/ebvh_nodes_sort_sah_local4.js +4 -0
- package/src/core/bvh2/bvh3/ebvh_optimize_treelet.d.ts +2 -2
- package/src/core/bvh2/bvh3/ebvh_optimize_treelet.d.ts.map +1 -1
- package/src/core/bvh2/bvh3/ebvh_optimize_treelet.js +2 -2
- package/src/core/collection/table/RowFirstTable.d.ts +29 -50
- package/src/core/collection/table/RowFirstTable.d.ts.map +1 -1
- package/src/core/collection/table/RowFirstTable.js +107 -121
- package/src/engine/graphics/sh3/path_tracer/BufferedGeometryBVH.d.ts.map +1 -1
- package/src/engine/graphics/sh3/path_tracer/BufferedGeometryBVH.js +4 -0
- package/src/engine/graphics/sh3/path_tracer/PathTracedScene.d.ts.map +1 -1
- package/src/engine/graphics/sh3/path_tracer/PathTracedScene.js +3 -4
package/package.json
CHANGED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
export const ALLOCATOR_NO_SPACE: 4294967295;
|
|
2
|
+
/**
|
|
3
|
+
* Fast hard realtime O(1) offset allocator with minimal fragmentation.
|
|
4
|
+
*
|
|
5
|
+
* Uses 256 bins with 8 bit floating point distribution (3 bit mantissa + 5 bit exponent) and a two level bitfield to find the next available bin using 2x LZCNT instructions to make all operations O(1). Bin sizes following the floating point distribution ensures hard bounds for memory overhead percentage regardless of size class. Pow2 bins would waste up to +100% memory (+50% on average). Our float bins waste up to +12.5% (+6.25% on average).
|
|
6
|
+
*
|
|
7
|
+
* The allocation metadata is stored in a separate data structure, making this allocator suitable for sub-allocating any resources, such as GPU heaps, buffers and arrays. Returns an offset to the first element of the allocated contiguous range.
|
|
8
|
+
*
|
|
9
|
+
* @see "A comparison of memory allocators for real-time applications" by Miguel Masmano et al
|
|
10
|
+
* @example
|
|
11
|
+
* const allocator = new Allocator(12345); // Allocator with 12345 contiguous elements in total
|
|
12
|
+
*
|
|
13
|
+
* const a = allocator.allocate(1337); // Allocate a 1337 element contiguous range
|
|
14
|
+
* const offset_a = a.offset; // Provides offset to the first element of the range
|
|
15
|
+
* do_something(offset_a);
|
|
16
|
+
*
|
|
17
|
+
* const b = allocator.allocate(123); // Allocate a 123 element contiguous range
|
|
18
|
+
* const offset_b = b.offset; // Provides offset to the first element of the range
|
|
19
|
+
* do_something(offset_b);
|
|
20
|
+
*
|
|
21
|
+
* allocator.free(a); // Free allocation a
|
|
22
|
+
* allocator.free(b); // Free allocation b
|
|
23
|
+
*/
|
|
24
|
+
export class OffsetAllocator {
|
|
25
|
+
/**
|
|
26
|
+
*
|
|
27
|
+
* @param {number} size
|
|
28
|
+
* @param {number} maxAllocs
|
|
29
|
+
*/
|
|
30
|
+
constructor(size: number, maxAllocs?: number);
|
|
31
|
+
/**
|
|
32
|
+
* Total managed space
|
|
33
|
+
* uint32
|
|
34
|
+
* @type {number}
|
|
35
|
+
*/
|
|
36
|
+
size: number;
|
|
37
|
+
/**
|
|
38
|
+
* Limit on number of allocations
|
|
39
|
+
* uint32
|
|
40
|
+
* @type {number}
|
|
41
|
+
*/
|
|
42
|
+
maxAllocs: number;
|
|
43
|
+
/**
|
|
44
|
+
* uint32
|
|
45
|
+
* @type {number}
|
|
46
|
+
*/
|
|
47
|
+
freeStorage: number;
|
|
48
|
+
/**
|
|
49
|
+
* uint32
|
|
50
|
+
* @type {number}
|
|
51
|
+
*/
|
|
52
|
+
usedBinsTop: number;
|
|
53
|
+
/**
|
|
54
|
+
*
|
|
55
|
+
* @type {Uint8Array}
|
|
56
|
+
*/
|
|
57
|
+
usedBins: Uint8Array;
|
|
58
|
+
/**
|
|
59
|
+
*
|
|
60
|
+
* @type {Uint32Array}
|
|
61
|
+
*/
|
|
62
|
+
binIndices: Uint32Array;
|
|
63
|
+
/**
|
|
64
|
+
*
|
|
65
|
+
* @type {RowFirstTable}
|
|
66
|
+
*/
|
|
67
|
+
nodes: RowFirstTable;
|
|
68
|
+
/**
|
|
69
|
+
* @type {Uint32Array}
|
|
70
|
+
*/
|
|
71
|
+
freeNodes: Uint32Array;
|
|
72
|
+
/**
|
|
73
|
+
* uint32
|
|
74
|
+
* @type {number}
|
|
75
|
+
*/
|
|
76
|
+
freeOffset: number;
|
|
77
|
+
/**
|
|
78
|
+
*
|
|
79
|
+
* @param {number} size
|
|
80
|
+
* @returns {Allocation}
|
|
81
|
+
*/
|
|
82
|
+
allocate(size: number): Allocation;
|
|
83
|
+
/**
|
|
84
|
+
*
|
|
85
|
+
* @param {Allocation} allocation
|
|
86
|
+
*/
|
|
87
|
+
free(allocation: Allocation): void;
|
|
88
|
+
reset(): void;
|
|
89
|
+
/**
|
|
90
|
+
* Useful for debug and UI
|
|
91
|
+
* @return {StorageReport}
|
|
92
|
+
*/
|
|
93
|
+
storageReport(): StorageReport;
|
|
94
|
+
#private;
|
|
95
|
+
}
|
|
96
|
+
import { RowFirstTable } from "../../collection/table/RowFirstTable.js";
|
|
97
|
+
declare class Allocation {
|
|
98
|
+
/**
|
|
99
|
+
*
|
|
100
|
+
* @param {number} offset
|
|
101
|
+
* @param {number} metadata
|
|
102
|
+
* @return {Allocation}
|
|
103
|
+
*/
|
|
104
|
+
static from(offset: number, metadata: number): Allocation;
|
|
105
|
+
offset: number;
|
|
106
|
+
metadata: number;
|
|
107
|
+
}
|
|
108
|
+
declare class StorageReport {
|
|
109
|
+
totalFreeSpace: number;
|
|
110
|
+
largestFreeRegion: number;
|
|
111
|
+
}
|
|
112
|
+
export {};
|
|
113
|
+
//# sourceMappingURL=OffsetAllocator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OffsetAllocator.d.ts","sourceRoot":"","sources":["../../../../../src/core/binary/allocator/OffsetAllocator.js"],"names":[],"mappings":"AAaA,iCAAkC,UAAU,CAAC;AAmF7C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH;IAwDI;;;;OAIG;IACH,kBAHW,MAAM,cACN,MAAM,EAYhB;IAtED;;;;OAIG;IACH,MAFU,MAAM,CAEP;IAET;;;;OAIG;IACH,WAFU,MAAM,CAEF;IAEd;;;OAGG;IACH,aAFU,MAAM,CAEA;IAEhB;;;OAGG;IACH,aAFU,MAAM,CAEA;IAEhB;;;OAGG;IACH,UAFU,UAAU,CAEoB;IAExC;;;OAGG;IACH,YAFU,WAAW,CAEuB;IAE5C;;;OAGG;IACH,OAFU,aAAa,CAEmB;IAE1C;;OAEG;IACH,WAFU,WAAW,CAEX;IAEV;;;OAGG;IACH,YAFU,MAAM,CAED;IAmBf;;;;OAIG;IACH,eAHW,MAAM,GACJ,UAAU,CAkGtB;IAED;;;OAGG;IACH,iBAFW,UAAU,QAsFpB;IAqID,cA4BC;IAED;;;OAGG;IACH,iBAFY,aAAa,CA2BxB;;CACJ;8BA9jB6B,yCAAyC;AAgDvE;IAII;;;;;OAKG;IACH,oBAJW,MAAM,YACN,MAAM,GACL,UAAU,CASrB;IAhBD,eAA4B;IAC5B,iBAA8B;CAgBjC;AAoBD;IACI,uBAAkB;IAClB,0BAAqB;CACxB"}
|
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
import { assert } from "../../assert.js";
|
|
2
|
+
import { RowFirstTable } from "../../collection/table/RowFirstTable.js";
|
|
3
|
+
import { RowFirstTableSpec } from "../../collection/table/RowFirstTableSpec.js";
|
|
4
|
+
import { ctz32 } from "../ctz32.js";
|
|
5
|
+
import { floatToUint, uintToFloatRoundDown, uintToFloatRoundUp } from "../float/SmallFloat.js";
|
|
6
|
+
import { msb_32 } from "../msb_32.js";
|
|
7
|
+
import { BinaryDataType } from "../type/BinaryDataType.js";
|
|
8
|
+
|
|
9
|
+
const NUM_TOP_BINS = 32;
|
|
10
|
+
const BINS_PER_LEAF = 8;
|
|
11
|
+
const TOP_BINS_INDEX_SHIFT = 3;
|
|
12
|
+
const LEAF_BINS_INDEX_MASK = 0x7;
|
|
13
|
+
const NUM_LEAF_BINS = NUM_TOP_BINS * BINS_PER_LEAF;
|
|
14
|
+
export const ALLOCATOR_NO_SPACE = 0xffffffff;
|
|
15
|
+
const NODE_UNUSED = 0xffffffff
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Can potentially use u16, but number of allocation slots will be limited to 65k, so a severe limitation for little gain
|
|
19
|
+
* @type {BinaryDataType}
|
|
20
|
+
*/
|
|
21
|
+
const NODE_INDEX_DATA_TYPE = BinaryDataType.Uint32;
|
|
22
|
+
|
|
23
|
+
const DEBUG_VERBOSE = false;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @see https://github.com/sebbbi/OffsetAllocator/blob/3610a7377088b1e8c8f1525f458c96038a4e6fc0/offsetAllocator.hpp#L70
|
|
27
|
+
* @type {RowFirstTableSpec}
|
|
28
|
+
*/
|
|
29
|
+
const NODE_TABLE_SPEC = RowFirstTableSpec.get([
|
|
30
|
+
BinaryDataType.Uint32, // Data Offset
|
|
31
|
+
BinaryDataType.Uint32, // Data Size
|
|
32
|
+
NODE_INDEX_DATA_TYPE, // Bin List Prev
|
|
33
|
+
NODE_INDEX_DATA_TYPE, // Bin List Next
|
|
34
|
+
NODE_INDEX_DATA_TYPE, // Neighbour Prev
|
|
35
|
+
NODE_INDEX_DATA_TYPE, // Neighbour Next
|
|
36
|
+
BinaryDataType.Uint8, // FLAGS
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const NODE_FLAGS_USED = 1;
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
const NODE_FIELD_DATA_OFFSET = 0;
|
|
43
|
+
const NODE_FIELD_DATA_SIZE = 1;
|
|
44
|
+
const NODE_FIELD_BIN_LIST_PREV = 2;
|
|
45
|
+
const NODE_FIELD_BIN_LIST_NEXT = 3;
|
|
46
|
+
const NODE_FIELD_NEIGHBOR_PREV = 4;
|
|
47
|
+
const NODE_FIELD_NEIGHBOR_NEXT = 5;
|
|
48
|
+
const NODE_FIELD_FLAGS = 6;
|
|
49
|
+
|
|
50
|
+
class Allocation {
|
|
51
|
+
offset = ALLOCATOR_NO_SPACE;
|
|
52
|
+
metadata = ALLOCATOR_NO_SPACE;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
*
|
|
56
|
+
* @param {number} offset
|
|
57
|
+
* @param {number} metadata
|
|
58
|
+
* @return {Allocation}
|
|
59
|
+
*/
|
|
60
|
+
static from(offset, metadata) {
|
|
61
|
+
const r = new Allocation();
|
|
62
|
+
|
|
63
|
+
r.offset = offset;
|
|
64
|
+
r.metadata = metadata;
|
|
65
|
+
|
|
66
|
+
return r;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
*
|
|
72
|
+
* @param {number} bitMask
|
|
73
|
+
* @param {number} startBitIndex
|
|
74
|
+
* @return {number}
|
|
75
|
+
*/
|
|
76
|
+
function findLowestSetBitAfter(bitMask, startBitIndex) {
|
|
77
|
+
const maskBeforeStartIndex = (1 << startBitIndex) - 1;
|
|
78
|
+
const maskAfterStartIndex = ~maskBeforeStartIndex;
|
|
79
|
+
const bitsAfter = bitMask & maskAfterStartIndex;
|
|
80
|
+
|
|
81
|
+
if (bitsAfter === 0) {
|
|
82
|
+
return ALLOCATOR_NO_SPACE;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return ctz32(bitsAfter);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
class StorageReport {
|
|
89
|
+
totalFreeSpace = 0
|
|
90
|
+
largestFreeRegion = 0
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/*
|
|
94
|
+
Largely a port of https://github.com/sebbbi/OffsetAllocator by "Sebastian Aaltonen"
|
|
95
|
+
*/
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Fast hard realtime O(1) offset allocator with minimal fragmentation.
|
|
99
|
+
*
|
|
100
|
+
* Uses 256 bins with 8 bit floating point distribution (3 bit mantissa + 5 bit exponent) and a two level bitfield to find the next available bin using 2x LZCNT instructions to make all operations O(1). Bin sizes following the floating point distribution ensures hard bounds for memory overhead percentage regardless of size class. Pow2 bins would waste up to +100% memory (+50% on average). Our float bins waste up to +12.5% (+6.25% on average).
|
|
101
|
+
*
|
|
102
|
+
* The allocation metadata is stored in a separate data structure, making this allocator suitable for sub-allocating any resources, such as GPU heaps, buffers and arrays. Returns an offset to the first element of the allocated contiguous range.
|
|
103
|
+
*
|
|
104
|
+
* @see "A comparison of memory allocators for real-time applications" by Miguel Masmano et al
|
|
105
|
+
* @example
|
|
106
|
+
* const allocator = new Allocator(12345); // Allocator with 12345 contiguous elements in total
|
|
107
|
+
*
|
|
108
|
+
* const a = allocator.allocate(1337); // Allocate a 1337 element contiguous range
|
|
109
|
+
* const offset_a = a.offset; // Provides offset to the first element of the range
|
|
110
|
+
* do_something(offset_a);
|
|
111
|
+
*
|
|
112
|
+
* const b = allocator.allocate(123); // Allocate a 123 element contiguous range
|
|
113
|
+
* const offset_b = b.offset; // Provides offset to the first element of the range
|
|
114
|
+
* do_something(offset_b);
|
|
115
|
+
*
|
|
116
|
+
* allocator.free(a); // Free allocation a
|
|
117
|
+
* allocator.free(b); // Free allocation b
|
|
118
|
+
*/
|
|
119
|
+
export class OffsetAllocator {
|
|
120
|
+
/**
|
|
121
|
+
* Total managed space
|
|
122
|
+
* uint32
|
|
123
|
+
* @type {number}
|
|
124
|
+
*/
|
|
125
|
+
size = 0;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Limit on number of allocations
|
|
129
|
+
* uint32
|
|
130
|
+
* @type {number}
|
|
131
|
+
*/
|
|
132
|
+
maxAllocs = 0;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* uint32
|
|
136
|
+
* @type {number}
|
|
137
|
+
*/
|
|
138
|
+
freeStorage = 0;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* uint32
|
|
142
|
+
* @type {number}
|
|
143
|
+
*/
|
|
144
|
+
usedBinsTop = 0;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
*
|
|
148
|
+
* @type {Uint8Array}
|
|
149
|
+
*/
|
|
150
|
+
usedBins = new Uint8Array(NUM_TOP_BINS);
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
*
|
|
154
|
+
* @type {Uint32Array}
|
|
155
|
+
*/
|
|
156
|
+
binIndices = new Uint32Array(NUM_LEAF_BINS);
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
*
|
|
160
|
+
* @type {RowFirstTable}
|
|
161
|
+
*/
|
|
162
|
+
nodes = new RowFirstTable(NODE_TABLE_SPEC)
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @type {Uint32Array}
|
|
166
|
+
*/
|
|
167
|
+
freeNodes;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* uint32
|
|
171
|
+
* @type {number}
|
|
172
|
+
*/
|
|
173
|
+
freeOffset = 0;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
*
|
|
177
|
+
* @param {number} size
|
|
178
|
+
* @param {number} maxAllocs
|
|
179
|
+
*/
|
|
180
|
+
constructor(size, maxAllocs = 128 * 1024) {
|
|
181
|
+
assert.isNonNegativeInteger(size, 'size');
|
|
182
|
+
assert.isNonNegativeInteger(maxAllocs, 'maxAllocs');
|
|
183
|
+
|
|
184
|
+
this.size = size;
|
|
185
|
+
this.maxAllocs = maxAllocs;
|
|
186
|
+
|
|
187
|
+
this.freeNodes = new Uint32Array(this.maxAllocs);
|
|
188
|
+
|
|
189
|
+
this.reset();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
*
|
|
194
|
+
* @param {number} size
|
|
195
|
+
* @returns {Allocation}
|
|
196
|
+
*/
|
|
197
|
+
allocate(size) {
|
|
198
|
+
assert.isNonNegativeInteger(size, 'size');
|
|
199
|
+
|
|
200
|
+
// Out of allocations?
|
|
201
|
+
if (this.freeOffset === 0) {
|
|
202
|
+
return Allocation.from(ALLOCATOR_NO_SPACE, ALLOCATOR_NO_SPACE);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Round up to bin index to ensure that alloc >= bin
|
|
206
|
+
// Gives us min bin index that fits the size
|
|
207
|
+
let minBinIndex = uintToFloatRoundUp(size);
|
|
208
|
+
|
|
209
|
+
const minTopBinIndex = minBinIndex >>> TOP_BINS_INDEX_SHIFT;
|
|
210
|
+
const minLeafBinIndex = minBinIndex & LEAF_BINS_INDEX_MASK;
|
|
211
|
+
|
|
212
|
+
let topBinIndex = minTopBinIndex;
|
|
213
|
+
let leafBinIndex = ALLOCATOR_NO_SPACE;
|
|
214
|
+
|
|
215
|
+
// If top bin exists, scan its leaf bin. This can fail (NO_SPACE).
|
|
216
|
+
if ((this.usedBinsTop & (1 << topBinIndex)) !== 0) {
|
|
217
|
+
leafBinIndex = findLowestSetBitAfter(this.usedBins[topBinIndex], minLeafBinIndex);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// If we didn't find space in top bin, we search top bin from +1
|
|
221
|
+
if (leafBinIndex === ALLOCATOR_NO_SPACE) {
|
|
222
|
+
topBinIndex = findLowestSetBitAfter(this.usedBinsTop, minTopBinIndex + 1);
|
|
223
|
+
|
|
224
|
+
// Out of space?
|
|
225
|
+
if (topBinIndex === ALLOCATOR_NO_SPACE) {
|
|
226
|
+
return Allocation.from(ALLOCATOR_NO_SPACE, ALLOCATOR_NO_SPACE);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// All leaf bins here fit the alloc, since the top bin was rounded up. Start leaf search from bit 0.
|
|
230
|
+
// NOTE: This search can't fail since at least one leaf bit was set because the top bit was set.
|
|
231
|
+
leafBinIndex = ctz32(this.usedBins[topBinIndex]);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
let bin_index = ((topBinIndex << TOP_BINS_INDEX_SHIFT) | leafBinIndex) >>> 0; // ">>> 0" shift is to force UINT32
|
|
235
|
+
|
|
236
|
+
// Pop the top node of the bin. Bin top = node.next.
|
|
237
|
+
let node_index = this.binIndices[bin_index];
|
|
238
|
+
|
|
239
|
+
const nodes = this.nodes;
|
|
240
|
+
|
|
241
|
+
const nodeTotalSize = nodes.readCellValue(node_index, NODE_FIELD_DATA_SIZE);
|
|
242
|
+
nodes.writeCellValue(node_index, NODE_FIELD_DATA_SIZE, size);
|
|
243
|
+
nodes.writeCellValue(node_index, NODE_FIELD_FLAGS, NODE_FLAGS_USED); //mark as USED
|
|
244
|
+
|
|
245
|
+
const node_bin_list_next = nodes.readCellValue(node_index, NODE_FIELD_BIN_LIST_NEXT);
|
|
246
|
+
this.binIndices[bin_index] = node_bin_list_next;
|
|
247
|
+
|
|
248
|
+
if (node_bin_list_next !== NODE_UNUSED) {
|
|
249
|
+
nodes.writeCellValue(node_bin_list_next, NODE_FIELD_BIN_LIST_PREV, NODE_UNUSED);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
this.freeStorage -= nodeTotalSize;
|
|
253
|
+
|
|
254
|
+
if (DEBUG_VERBOSE) {
|
|
255
|
+
console.log(`Free storage: ${this.freeStorage} (-${nodeTotalSize}) (allocate)`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Bin empty?
|
|
259
|
+
if (this.binIndices[bin_index] === NODE_UNUSED) {
|
|
260
|
+
// Remove a leaf bin mask bit
|
|
261
|
+
this.usedBins[topBinIndex] &= ~(1 << leafBinIndex);
|
|
262
|
+
|
|
263
|
+
// All leaf bins empty?
|
|
264
|
+
if (this.usedBins[topBinIndex] === 0) {
|
|
265
|
+
// Remove a top bin mask bit
|
|
266
|
+
this.usedBinsTop &= ~(1 << topBinIndex);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Push back reminder N elements to a lower bin
|
|
271
|
+
const reminderSize = nodeTotalSize - size;
|
|
272
|
+
|
|
273
|
+
const node_data_offset = nodes.readCellValue(node_index, NODE_FIELD_DATA_OFFSET);
|
|
274
|
+
|
|
275
|
+
if (reminderSize > 0) {
|
|
276
|
+
const new_node_index = this.#insertNodeIntoBin(reminderSize, node_data_offset + size);
|
|
277
|
+
|
|
278
|
+
const node_neighbor_next = nodes.readCellValue(node_index, NODE_FIELD_NEIGHBOR_NEXT);
|
|
279
|
+
|
|
280
|
+
// Link nodes next to each other so that we can merge them later if both are free
|
|
281
|
+
// And update the old next neighbor to point to the new node (in middle)
|
|
282
|
+
if (node_neighbor_next !== NODE_UNUSED) {
|
|
283
|
+
nodes.writeCellValue(node_neighbor_next, NODE_FIELD_NEIGHBOR_PREV, new_node_index);
|
|
284
|
+
}
|
|
285
|
+
nodes.writeCellValue(new_node_index, NODE_FIELD_NEIGHBOR_PREV, node_index);
|
|
286
|
+
nodes.writeCellValue(new_node_index, NODE_FIELD_NEIGHBOR_NEXT, node_neighbor_next);
|
|
287
|
+
|
|
288
|
+
nodes.writeCellValue(node_index, NODE_FIELD_NEIGHBOR_NEXT, new_node_index);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
return Allocation.from(node_data_offset, node_index);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
*
|
|
297
|
+
* @param {Allocation} allocation
|
|
298
|
+
*/
|
|
299
|
+
free(allocation) {
|
|
300
|
+
assert.notEqual(allocation.metadata, ALLOCATOR_NO_SPACE, 'Invalid allocation');
|
|
301
|
+
// if (!m_nodes) return;
|
|
302
|
+
|
|
303
|
+
const node_index = allocation.metadata;
|
|
304
|
+
|
|
305
|
+
const nodes = this.nodes;
|
|
306
|
+
const node_flags = nodes.readCellValue(node_index, NODE_FIELD_FLAGS);
|
|
307
|
+
|
|
308
|
+
// Double delete check
|
|
309
|
+
assert.notEqual(node_flags & NODE_FLAGS_USED, 0, 'Node is not in use');
|
|
310
|
+
|
|
311
|
+
// Merge with neighbors...
|
|
312
|
+
let offset = nodes.readCellValue(node_index, NODE_FIELD_DATA_OFFSET)
|
|
313
|
+
let size = nodes.readCellValue(node_index, NODE_FIELD_DATA_SIZE);
|
|
314
|
+
|
|
315
|
+
const node_neighbour_prev = nodes.readCellValue(node_index, NODE_FIELD_NEIGHBOR_PREV);
|
|
316
|
+
|
|
317
|
+
if (
|
|
318
|
+
(node_neighbour_prev !== NODE_UNUSED)
|
|
319
|
+
&& ((nodes.readCellValue(node_neighbour_prev, NODE_FIELD_FLAGS) & NODE_FLAGS_USED) === 0) // not used
|
|
320
|
+
) {
|
|
321
|
+
// Previous (contiguous) free node: Change offset to previous node offset. Sum sizes
|
|
322
|
+
|
|
323
|
+
const previous_offset = nodes.readCellValue(node_neighbour_prev, NODE_FIELD_DATA_OFFSET);
|
|
324
|
+
const previous_size = nodes.readCellValue(node_neighbour_prev, NODE_FIELD_DATA_SIZE);
|
|
325
|
+
|
|
326
|
+
offset = previous_offset;
|
|
327
|
+
size += previous_size;
|
|
328
|
+
|
|
329
|
+
// Remove node from the bin linked list and put it in the freelist
|
|
330
|
+
this.#removeNodeFromBin(node_neighbour_prev);
|
|
331
|
+
|
|
332
|
+
assert.equal(nodes.readCellValue(node_neighbour_prev, NODE_FIELD_NEIGHBOR_NEXT), node_index);
|
|
333
|
+
|
|
334
|
+
nodes.writeCellValue(
|
|
335
|
+
node_index, NODE_FIELD_NEIGHBOR_PREV,
|
|
336
|
+
nodes.readCellValue(node_neighbour_prev, NODE_FIELD_NEIGHBOR_PREV)
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const node_neighbour_next = nodes.readCellValue(node_index, NODE_FIELD_NEIGHBOR_NEXT);
|
|
341
|
+
|
|
342
|
+
if (
|
|
343
|
+
(node_neighbour_next !== NODE_UNUSED)
|
|
344
|
+
&& ((nodes.readCellValue(node_neighbour_next, NODE_FIELD_FLAGS) & NODE_FLAGS_USED) === 0) // not used
|
|
345
|
+
) {
|
|
346
|
+
// Next (contiguous) free node: Offset remains the same. Sum sizes.
|
|
347
|
+
|
|
348
|
+
size += nodes.readCellValue(node_neighbour_next, NODE_FIELD_DATA_SIZE);
|
|
349
|
+
|
|
350
|
+
// Remove node from the bin linked list and put it in the freelist
|
|
351
|
+
this.#removeNodeFromBin(node_neighbour_next);
|
|
352
|
+
|
|
353
|
+
assert.equal(nodes.readCellValue(node_neighbour_next, NODE_FIELD_NEIGHBOR_PREV), node_index);
|
|
354
|
+
|
|
355
|
+
nodes.writeCellValue(
|
|
356
|
+
node_index, NODE_FIELD_NEIGHBOR_NEXT,
|
|
357
|
+
nodes.readCellValue(node_neighbour_next, NODE_FIELD_NEIGHBOR_NEXT)
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const neighborNext = nodes.readCellValue(node_index, NODE_FIELD_NEIGHBOR_NEXT);
|
|
362
|
+
const neighborPrev = nodes.readCellValue(node_index, NODE_FIELD_NEIGHBOR_PREV);
|
|
363
|
+
|
|
364
|
+
// Insert the removed node to freelist
|
|
365
|
+
if (DEBUG_VERBOSE) {
|
|
366
|
+
console.log(`Putting node ${node_index} into freelist[${this.freeOffset + 1}] (free)`);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
this.freeNodes[++this.freeOffset] = node_index;
|
|
370
|
+
|
|
371
|
+
// Insert the (combined) free node to bin
|
|
372
|
+
const combinedNodeIndex = this.#insertNodeIntoBin(size, offset);
|
|
373
|
+
|
|
374
|
+
// Connect neighbors with the new combined node
|
|
375
|
+
if (neighborNext !== NODE_UNUSED) {
|
|
376
|
+
nodes.writeCellValue(combinedNodeIndex, NODE_FIELD_NEIGHBOR_NEXT, neighborNext);
|
|
377
|
+
nodes.writeCellValue(neighborNext, NODE_FIELD_NEIGHBOR_PREV, combinedNodeIndex);
|
|
378
|
+
}
|
|
379
|
+
if (neighborPrev !== NODE_UNUSED) {
|
|
380
|
+
nodes.writeCellValue(combinedNodeIndex, NODE_FIELD_NEIGHBOR_PREV, neighborPrev);
|
|
381
|
+
nodes.writeCellValue(neighborPrev, NODE_FIELD_NEIGHBOR_NEXT, combinedNodeIndex);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
*
|
|
387
|
+
* @param {number} size
|
|
388
|
+
* @param {number} data_offset
|
|
389
|
+
* @returns {number}
|
|
390
|
+
*/
|
|
391
|
+
#insertNodeIntoBin(size, data_offset) {
|
|
392
|
+
assert.isNonNegativeInteger(size, 'size');
|
|
393
|
+
assert.isNonNegativeInteger(data_offset, 'size');
|
|
394
|
+
|
|
395
|
+
// Round down to bin index to ensure that bin >= alloc
|
|
396
|
+
const bin_index = uintToFloatRoundDown(size);
|
|
397
|
+
|
|
398
|
+
const top_bin_index = bin_index >>> TOP_BINS_INDEX_SHIFT;
|
|
399
|
+
const leafBinIndex = bin_index & LEAF_BINS_INDEX_MASK;
|
|
400
|
+
|
|
401
|
+
// Bin was empty before?
|
|
402
|
+
if (this.binIndices[bin_index] === NODE_UNUSED) {
|
|
403
|
+
// Set bin mask bits
|
|
404
|
+
this.usedBins[top_bin_index] |= 1 << leafBinIndex;
|
|
405
|
+
this.usedBinsTop |= 1 << top_bin_index;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Take a freelist node and insert on top of the bin linked list (next = old top)
|
|
409
|
+
const top_node_index = this.binIndices[bin_index];
|
|
410
|
+
const node_index = this.freeNodes[this.freeOffset--];
|
|
411
|
+
|
|
412
|
+
if (DEBUG_VERBOSE) {
|
|
413
|
+
console.log(`Getting node ${node_index} from freelist[${this.freeOffset + 1}]`);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const nodes = this.nodes;
|
|
417
|
+
|
|
418
|
+
// initialize node
|
|
419
|
+
|
|
420
|
+
nodes.writeCellValue(node_index, NODE_FIELD_DATA_OFFSET, data_offset);
|
|
421
|
+
nodes.writeCellValue(node_index, NODE_FIELD_DATA_SIZE, size);
|
|
422
|
+
|
|
423
|
+
nodes.writeCellValue(node_index, NODE_FIELD_BIN_LIST_PREV, NODE_UNUSED);
|
|
424
|
+
nodes.writeCellValue(node_index, NODE_FIELD_BIN_LIST_NEXT, top_node_index);
|
|
425
|
+
|
|
426
|
+
nodes.writeCellValue(node_index, NODE_FIELD_NEIGHBOR_PREV, NODE_UNUSED);
|
|
427
|
+
nodes.writeCellValue(node_index, NODE_FIELD_NEIGHBOR_NEXT, NODE_UNUSED);
|
|
428
|
+
|
|
429
|
+
nodes.writeCellValue(node_index, NODE_FIELD_FLAGS, 0);
|
|
430
|
+
|
|
431
|
+
if (top_node_index !== NODE_UNUSED) {
|
|
432
|
+
nodes.writeCellValue(top_node_index, NODE_FIELD_BIN_LIST_PREV, node_index);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
this.binIndices[bin_index] = node_index;
|
|
436
|
+
|
|
437
|
+
this.freeStorage += size;
|
|
438
|
+
|
|
439
|
+
if (DEBUG_VERBOSE) {
|
|
440
|
+
console.log(`Free storage: ${this.freeStorage} (+${size}) (insertNodeIntoBin)`);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return node_index;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
*
|
|
448
|
+
* @param {number} node_index
|
|
449
|
+
*/
|
|
450
|
+
#removeNodeFromBin(node_index) {
|
|
451
|
+
assert.isNonNegativeInteger(node_index, 'node_index');
|
|
452
|
+
|
|
453
|
+
const nodes = this.nodes;
|
|
454
|
+
const node_bin_list_prev = nodes.readCellValue(node_index, NODE_FIELD_BIN_LIST_PREV);
|
|
455
|
+
if (node_bin_list_prev !== NODE_UNUSED) {
|
|
456
|
+
// Easy case: We have previous node. Just remove this node from the middle of the list.
|
|
457
|
+
const node_bin_list_next = nodes.readCellValue(node_index, NODE_FIELD_BIN_LIST_NEXT);
|
|
458
|
+
nodes.writeCellValue(
|
|
459
|
+
node_bin_list_prev, NODE_FIELD_BIN_LIST_NEXT,
|
|
460
|
+
node_bin_list_next
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
if (node_bin_list_next !== NODE_UNUSED) {
|
|
464
|
+
nodes.writeCellValue(
|
|
465
|
+
node_bin_list_next, NODE_FIELD_BIN_LIST_PREV,
|
|
466
|
+
node_bin_list_prev
|
|
467
|
+
)
|
|
468
|
+
}
|
|
469
|
+
} else {
|
|
470
|
+
// Hard case: We are the first node in a bin. Find the bin.
|
|
471
|
+
|
|
472
|
+
// Round down to bin index to ensure that bin >= alloc
|
|
473
|
+
const node_data_size = nodes.readCellValue(node_index, NODE_FIELD_DATA_SIZE);
|
|
474
|
+
const binIndex = uintToFloatRoundDown(node_data_size);
|
|
475
|
+
|
|
476
|
+
const topBinIndex = binIndex >> TOP_BINS_INDEX_SHIFT;
|
|
477
|
+
const leafBinIndex = binIndex & LEAF_BINS_INDEX_MASK;
|
|
478
|
+
|
|
479
|
+
const node_bin_list_next = nodes.readCellValue(node_index, NODE_FIELD_BIN_LIST_NEXT);
|
|
480
|
+
this.binIndices[binIndex] = node_bin_list_next;
|
|
481
|
+
if (node_bin_list_next !== NODE_UNUSED) {
|
|
482
|
+
nodes.writeCellValue(
|
|
483
|
+
node_bin_list_next, NODE_FIELD_BIN_LIST_PREV,
|
|
484
|
+
NODE_UNUSED
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Bin empty?
|
|
489
|
+
if (this.binIndices[binIndex] === NODE_UNUSED) {
|
|
490
|
+
// Remove a leaf bin mask bit
|
|
491
|
+
this.usedBins[topBinIndex] &= ~(1 << leafBinIndex);
|
|
492
|
+
|
|
493
|
+
// All leaf bins empty?
|
|
494
|
+
if (this.usedBins[topBinIndex] === 0) {
|
|
495
|
+
// Remove a top bin mask bit
|
|
496
|
+
this.usedBinsTop &= ~(1 << topBinIndex);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Insert the node to freelist
|
|
502
|
+
if (DEBUG_VERBOSE) {
|
|
503
|
+
console.log(`Putting node ${node_index} into freelist[${this.freeOffset + 1}] (removeNodeFromBin)`);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
this.freeNodes[++this.freeOffset] = node_index;
|
|
507
|
+
|
|
508
|
+
const node_data_size = nodes.readCellValue(node_index, NODE_FIELD_DATA_SIZE);
|
|
509
|
+
this.freeStorage -= node_data_size;
|
|
510
|
+
|
|
511
|
+
if (DEBUG_VERBOSE) {
|
|
512
|
+
console.log(`Free storage: ${this.freeStorage} (-${node_data_size}) (removeNodeFromBin)`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
reset() {
|
|
517
|
+
this.freeStorage = 0;
|
|
518
|
+
this.usedBinsTop = 0;
|
|
519
|
+
|
|
520
|
+
const maxAllocs = this.maxAllocs;
|
|
521
|
+
|
|
522
|
+
this.freeOffset = maxAllocs - 1;
|
|
523
|
+
|
|
524
|
+
for (let i = 0; i < NUM_TOP_BINS; i++) {
|
|
525
|
+
this.usedBins[i] = 0;
|
|
526
|
+
}
|
|
527
|
+
for (let i = 0; i < NUM_LEAF_BINS; i++) {
|
|
528
|
+
this.binIndices[i] = NODE_UNUSED;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// drop data from the node table
|
|
532
|
+
this.nodes.length = 0;
|
|
533
|
+
this.nodes.setCapacity(maxAllocs);
|
|
534
|
+
|
|
535
|
+
// Freelist is a stack. Nodes in inverse order so that [0] pops first.
|
|
536
|
+
for (let i = 0; i < maxAllocs; i++) {
|
|
537
|
+
this.freeNodes[i] = maxAllocs - i - 1;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Start state: Whole storage as one big node
|
|
541
|
+
// Algorithm will split remainders and push them back as smaller nodes
|
|
542
|
+
this.#insertNodeIntoBin(this.size, 0);
|
|
543
|
+
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Useful for debug and UI
|
|
548
|
+
* @return {StorageReport}
|
|
549
|
+
*/
|
|
550
|
+
storageReport() {
|
|
551
|
+
let largestFreeRegion = 0;
|
|
552
|
+
let freeStorage = 0;
|
|
553
|
+
|
|
554
|
+
// Out of allocations? -> Zero free space
|
|
555
|
+
if (this.freeOffset > 0) {
|
|
556
|
+
freeStorage = this.freeStorage;
|
|
557
|
+
|
|
558
|
+
if (this.usedBinsTop !== 0) {
|
|
559
|
+
const topBinIndex = msb_32(this.usedBinsTop);
|
|
560
|
+
const leafBinIndex = msb_32(this.usedBins[topBinIndex]);
|
|
561
|
+
|
|
562
|
+
largestFreeRegion = floatToUint((topBinIndex << TOP_BINS_INDEX_SHIFT) | leafBinIndex);
|
|
563
|
+
|
|
564
|
+
assert.greaterThanOrEqual(freeStorage, largestFreeRegion);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const report = new StorageReport();
|
|
570
|
+
|
|
571
|
+
report.largestFreeRegion = largestFreeRegion;
|
|
572
|
+
report.totalFreeSpace = freeStorage;
|
|
573
|
+
|
|
574
|
+
return report;
|
|
575
|
+
}
|
|
576
|
+
}
|