@empty-sekai/sekai-custom-profile-sdk 0.3.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.
- package/LICENSE +235 -0
- package/LICENSE-EXCEPTION +32 -0
- package/NOTICE +11 -0
- package/README.en.md +456 -0
- package/README.md +458 -0
- package/dist/allium_renderer_wasm.js +14 -0
- package/dist/allium_renderer_wasm.wasm +0 -0
- package/dist/authoring.d.ts +14 -0
- package/dist/authoring.js +34 -0
- package/dist/cache/glyphPersistentCache.d.ts +145 -0
- package/dist/cache/glyphPersistentCache.js +346 -0
- package/dist/cache/indexedDbGlyphRecordStore.d.ts +26 -0
- package/dist/cache/indexedDbGlyphRecordStore.js +214 -0
- package/dist/cache/sessionImageResourceCache.d.ts +39 -0
- package/dist/cache/sessionImageResourceCache.js +156 -0
- package/dist/emscripten.d.ts +11 -0
- package/dist/emscripten.js +1 -0
- package/dist/fontProvider.d.ts +36 -0
- package/dist/fontProvider.js +84 -0
- package/dist/fontSdfAtlas.d.ts +92 -0
- package/dist/fontSdfAtlas.js +296 -0
- package/dist/gpu/browserSemanticResources.d.ts +64 -0
- package/dist/gpu/browserSemanticResources.js +232 -0
- package/dist/gpu/generalTextRenderPlacement.d.ts +8 -0
- package/dist/gpu/generalTextRenderPlacement.js +131 -0
- package/dist/gpu/previewTransformTextureLayout.d.ts +1 -0
- package/dist/gpu/previewTransformTextureLayout.js +12 -0
- package/dist/gpu/semanticCommandGeometry.d.ts +21 -0
- package/dist/gpu/semanticCommandGeometry.js +226 -0
- package/dist/gpu/semanticCommandPlanner.d.ts +131 -0
- package/dist/gpu/semanticCommandPlanner.js +203 -0
- package/dist/gpu/semanticTextGlyphBridge.d.ts +3 -0
- package/dist/gpu/semanticTextGlyphBridge.js +22 -0
- package/dist/gpu/semanticWebglSceneRenderer.d.ts +70 -0
- package/dist/gpu/semanticWebglSceneRenderer.js +211 -0
- package/dist/gpu/slotRanges.d.ts +5 -0
- package/dist/gpu/slotRanges.js +14 -0
- package/dist/gpu/webglSdfAtlasTexture.d.ts +17 -0
- package/dist/gpu/webglSdfAtlasTexture.js +72 -0
- package/dist/gpu/webglSdfGlyphPipeline.d.ts +26 -0
- package/dist/gpu/webglSdfGlyphPipeline.js +267 -0
- package/dist/gpu/webglSemanticCommandExecutor.d.ts +79 -0
- package/dist/gpu/webglSemanticCommandExecutor.js +686 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +8 -0
- package/dist/interaction/numericTextRegions.d.ts +46 -0
- package/dist/interaction/numericTextRegions.js +44 -0
- package/dist/localizationProvider.d.ts +33 -0
- package/dist/localizationProvider.js +74 -0
- package/dist/originPrebuiltSdfAtlasPackage.d.ts +39 -0
- package/dist/originPrebuiltSdfAtlasPackage.js +316 -0
- package/dist/prebuiltSdfAtlas.d.ts +36 -0
- package/dist/prebuiltSdfAtlas.js +261 -0
- package/dist/protocol.d.ts +456 -0
- package/dist/protocol.js +1 -0
- package/dist/renderer.d.ts +218 -0
- package/dist/renderer.js +641 -0
- package/dist/resourceProvider.d.ts +24 -0
- package/dist/resourceProvider.js +41 -0
- package/dist/telemetry/rendererTelemetry.d.ts +158 -0
- package/dist/telemetry/rendererTelemetry.js +232 -0
- package/dist/third-party/freetype/FTL.txt +169 -0
- package/dist/types/atlas.d.ts +90 -0
- package/dist/types/atlas.js +1 -0
- package/dist/types/authoring.d.ts +81 -0
- package/dist/types/authoring.js +1 -0
- package/dist/types/core.d.ts +128 -0
- package/dist/types/core.js +1 -0
- package/dist/types/freeType.d.ts +54 -0
- package/dist/types/freeType.js +13 -0
- package/dist/types/glyph.d.ts +33 -0
- package/dist/types/glyph.js +1 -0
- package/dist/types/layout.d.ts +25 -0
- package/dist/types/layout.js +1 -0
- package/dist/types/text.d.ts +28 -0
- package/dist/types/text.js +1 -0
- package/dist/worker/glyphWorkScheduler.d.ts +29 -0
- package/dist/worker/glyphWorkScheduler.js +107 -0
- package/dist/worker-client.d.ts +114 -0
- package/dist/worker-client.js +413 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +708 -0
- package/package.json +46 -0
- package/src/atlas.rs +652 -0
- package/src/authoring.ts +42 -0
- package/src/authoring_runtime.rs +303 -0
- package/src/cache/glyphPersistentCache.ts +463 -0
- package/src/cache/indexedDbGlyphRecordStore.ts +238 -0
- package/src/cache/sessionImageResourceCache.ts +186 -0
- package/src/edt.rs +97 -0
- package/src/emscripten.ts +17 -0
- package/src/fontProvider.ts +127 -0
- package/src/fontSdfAtlas.ts +431 -0
- package/src/geometry.rs +149 -0
- package/src/glyph_plan.rs +226 -0
- package/src/gpu/browserSemanticResources.ts +294 -0
- package/src/gpu/generalTextRenderPlacement.ts +175 -0
- package/src/gpu/previewTransformTextureLayout.ts +12 -0
- package/src/gpu/semanticCommandGeometry.ts +260 -0
- package/src/gpu/semanticCommandPlanner.ts +280 -0
- package/src/gpu/semanticTextGlyphBridge.ts +35 -0
- package/src/gpu/semanticWebglSceneRenderer.ts +261 -0
- package/src/gpu/slotRanges.ts +15 -0
- package/src/gpu/webglSdfAtlasTexture.ts +69 -0
- package/src/gpu/webglSdfGlyphPipeline.ts +299 -0
- package/src/gpu/webglSemanticCommandExecutor.ts +739 -0
- package/src/index.ts +84 -0
- package/src/interaction/numericTextRegions.ts +80 -0
- package/src/layout.rs +1882 -0
- package/src/lib.rs +1443 -0
- package/src/localizationProvider.ts +113 -0
- package/src/main.rs +55 -0
- package/src/masterdata_runtime.rs +496 -0
- package/src/originPrebuiltSdfAtlasPackage.ts +403 -0
- package/src/prebuiltSdfAtlas.ts +306 -0
- package/src/protocol.ts +150 -0
- package/src/renderer.ts +792 -0
- package/src/resourceProvider.ts +69 -0
- package/src/scene.rs +665 -0
- package/src/telemetry/rendererTelemetry.ts +358 -0
- package/src/types/atlas.ts +80 -0
- package/src/types/authoring.ts +85 -0
- package/src/types/core.ts +110 -0
- package/src/types/freeType.ts +63 -0
- package/src/types/glyph.ts +33 -0
- package/src/types/layout.ts +28 -0
- package/src/types/text.ts +28 -0
- package/src/worker/glyphWorkScheduler.ts +130 -0
- package/src/worker-client.ts +484 -0
- package/src/worker.ts +895 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { GLYPH_CACHE_SCHEMA, GlyphPersistentCache, } from "./glyphPersistentCache.js";
|
|
2
|
+
const DATABASE_NAME = "sekai-custom-profile-sdk-glyph-cache-v1";
|
|
3
|
+
const DATABASE_VERSION = 1;
|
|
4
|
+
const GLYPH_STORE = "glyphs";
|
|
5
|
+
const META_STORE = "meta";
|
|
6
|
+
const TOTALS_KEY = "totals";
|
|
7
|
+
export class IndexedDbGlyphRecordStore {
|
|
8
|
+
databaseName;
|
|
9
|
+
databasePromise = null;
|
|
10
|
+
constructor(databaseName = DATABASE_NAME) {
|
|
11
|
+
this.databaseName = databaseName;
|
|
12
|
+
}
|
|
13
|
+
async getMany(keys) {
|
|
14
|
+
if (keys.length === 0)
|
|
15
|
+
return [];
|
|
16
|
+
const database = await this.database();
|
|
17
|
+
const transaction = database.transaction(GLYPH_STORE, "readonly");
|
|
18
|
+
const store = transaction.objectStore(GLYPH_STORE);
|
|
19
|
+
const records = await Promise.all(keys.map((key) => request(store.get(key))));
|
|
20
|
+
await transactionDone(transaction);
|
|
21
|
+
return records.filter((record) => record != null);
|
|
22
|
+
}
|
|
23
|
+
async putMany(records, token) {
|
|
24
|
+
if (records.length === 0)
|
|
25
|
+
return true;
|
|
26
|
+
const database = await this.database();
|
|
27
|
+
const transaction = database.transaction([GLYPH_STORE, META_STORE], "readwrite", { durability: "relaxed" });
|
|
28
|
+
const glyphs = transaction.objectStore(GLYPH_STORE);
|
|
29
|
+
const meta = transaction.objectStore(META_STORE);
|
|
30
|
+
const totals = await readTotals(meta);
|
|
31
|
+
if (token && token.startedAtMs <= totals.lastClearMs) {
|
|
32
|
+
transaction.abort();
|
|
33
|
+
try {
|
|
34
|
+
await transactionDone(transaction);
|
|
35
|
+
}
|
|
36
|
+
catch { /* intentional abort */ }
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
for (const record of records) {
|
|
40
|
+
const existing = await request(glyphs.get(record.opaqueKey));
|
|
41
|
+
if (existing) {
|
|
42
|
+
totals.bytes -= existing.byteSize;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
totals.entries += 1;
|
|
46
|
+
}
|
|
47
|
+
totals.bytes += record.byteSize;
|
|
48
|
+
glyphs.put(record);
|
|
49
|
+
}
|
|
50
|
+
meta.put(totals);
|
|
51
|
+
await transactionDone(transaction);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
async deleteMany(keys) {
|
|
55
|
+
if (keys.length === 0)
|
|
56
|
+
return;
|
|
57
|
+
const database = await this.database();
|
|
58
|
+
const transaction = database.transaction([GLYPH_STORE, META_STORE], "readwrite", { durability: "relaxed" });
|
|
59
|
+
const glyphs = transaction.objectStore(GLYPH_STORE);
|
|
60
|
+
const meta = transaction.objectStore(META_STORE);
|
|
61
|
+
const totals = await readTotals(meta);
|
|
62
|
+
for (const key of keys) {
|
|
63
|
+
const existing = await request(glyphs.get(key));
|
|
64
|
+
if (!existing)
|
|
65
|
+
continue;
|
|
66
|
+
glyphs.delete(key);
|
|
67
|
+
totals.entries -= 1;
|
|
68
|
+
totals.bytes -= existing.byteSize;
|
|
69
|
+
}
|
|
70
|
+
meta.put(normalizeTotals(totals));
|
|
71
|
+
await transactionDone(transaction);
|
|
72
|
+
}
|
|
73
|
+
async stats() {
|
|
74
|
+
const database = await this.database();
|
|
75
|
+
const transaction = database.transaction(META_STORE, "readonly");
|
|
76
|
+
const totals = await readTotals(transaction.objectStore(META_STORE));
|
|
77
|
+
await transactionDone(transaction);
|
|
78
|
+
return { entries: totals.entries, bytes: totals.bytes };
|
|
79
|
+
}
|
|
80
|
+
async trimLruTo(maxBytes, protectedKeys = new Set()) {
|
|
81
|
+
const database = await this.database();
|
|
82
|
+
const transaction = database.transaction([GLYPH_STORE, META_STORE], "readwrite", { durability: "relaxed" });
|
|
83
|
+
const glyphs = transaction.objectStore(GLYPH_STORE);
|
|
84
|
+
const meta = transaction.objectStore(META_STORE);
|
|
85
|
+
const totals = await readTotals(meta);
|
|
86
|
+
let evicted = 0;
|
|
87
|
+
if (totals.bytes > maxBytes) {
|
|
88
|
+
const index = glyphs.index("lastAccessDay");
|
|
89
|
+
await iterateCursor(index.openCursor(), (cursor) => {
|
|
90
|
+
if (totals.bytes <= maxBytes)
|
|
91
|
+
return false;
|
|
92
|
+
const record = cursor.value;
|
|
93
|
+
if (!protectedKeys.has(record.opaqueKey)) {
|
|
94
|
+
cursor.delete();
|
|
95
|
+
totals.entries -= 1;
|
|
96
|
+
totals.bytes -= record.byteSize;
|
|
97
|
+
evicted += 1;
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
meta.put(normalizeTotals(totals));
|
|
103
|
+
await transactionDone(transaction);
|
|
104
|
+
return evicted;
|
|
105
|
+
}
|
|
106
|
+
async sweepBefore(lastAccessDayExclusive, maxRecords) {
|
|
107
|
+
const database = await this.database();
|
|
108
|
+
const transaction = database.transaction([GLYPH_STORE, META_STORE], "readwrite", { durability: "relaxed" });
|
|
109
|
+
const glyphs = transaction.objectStore(GLYPH_STORE);
|
|
110
|
+
const meta = transaction.objectStore(META_STORE);
|
|
111
|
+
const totals = await readTotals(meta);
|
|
112
|
+
let scanned = 0;
|
|
113
|
+
let deleted = 0;
|
|
114
|
+
await iterateCursor(glyphs.index("lastAccessDay").openCursor(), (cursor) => {
|
|
115
|
+
if (scanned >= maxRecords)
|
|
116
|
+
return false;
|
|
117
|
+
scanned += 1;
|
|
118
|
+
const record = cursor.value;
|
|
119
|
+
if (record.lastAccessDay >= lastAccessDayExclusive)
|
|
120
|
+
return false;
|
|
121
|
+
cursor.delete();
|
|
122
|
+
totals.entries -= 1;
|
|
123
|
+
totals.bytes -= record.byteSize;
|
|
124
|
+
deleted += 1;
|
|
125
|
+
return true;
|
|
126
|
+
});
|
|
127
|
+
meta.put(normalizeTotals(totals));
|
|
128
|
+
await transactionDone(transaction);
|
|
129
|
+
return { scanned, deleted };
|
|
130
|
+
}
|
|
131
|
+
async clear(clearedAtMs = Date.now()) {
|
|
132
|
+
const database = await this.database();
|
|
133
|
+
const transaction = database.transaction([GLYPH_STORE, META_STORE], "readwrite");
|
|
134
|
+
transaction.objectStore(GLYPH_STORE).clear();
|
|
135
|
+
const meta = transaction.objectStore(META_STORE);
|
|
136
|
+
const totals = await readTotals(meta);
|
|
137
|
+
meta.put({ key: TOTALS_KEY, entries: 0, bytes: 0, lastClearMs: Math.max(totals.lastClearMs, clearedAtMs) });
|
|
138
|
+
await transactionDone(transaction);
|
|
139
|
+
}
|
|
140
|
+
close() {
|
|
141
|
+
void this.databasePromise?.then((database) => database.close());
|
|
142
|
+
this.databasePromise = null;
|
|
143
|
+
}
|
|
144
|
+
database() {
|
|
145
|
+
if (typeof indexedDB === "undefined")
|
|
146
|
+
return Promise.reject(new Error("IndexedDB is unavailable"));
|
|
147
|
+
this.databasePromise ??= new Promise((resolve, reject) => {
|
|
148
|
+
const open = indexedDB.open(this.databaseName, DATABASE_VERSION);
|
|
149
|
+
open.onupgradeneeded = () => {
|
|
150
|
+
const database = open.result;
|
|
151
|
+
const glyphs = database.createObjectStore(GLYPH_STORE, { keyPath: "opaqueKey" });
|
|
152
|
+
glyphs.createIndex("lastAccessDay", "lastAccessDay");
|
|
153
|
+
database.createObjectStore(META_STORE, { keyPath: "key" });
|
|
154
|
+
};
|
|
155
|
+
open.onerror = () => reject(open.error ?? new Error("open IndexedDB glyph cache failed"));
|
|
156
|
+
open.onblocked = () => reject(new Error("IndexedDB glyph cache upgrade was blocked"));
|
|
157
|
+
open.onsuccess = () => {
|
|
158
|
+
open.result.onversionchange = () => open.result.close();
|
|
159
|
+
resolve(open.result);
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
return this.databasePromise;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
export function createOriginGlyphPersistentCache(options = {}) {
|
|
166
|
+
return new GlyphPersistentCache({
|
|
167
|
+
mode: "origin",
|
|
168
|
+
store: new IndexedDbGlyphRecordStore(options.databaseName),
|
|
169
|
+
softBytes: options.softBytes,
|
|
170
|
+
hardBytes: options.hardBytes,
|
|
171
|
+
ttlDays: options.ttlDays,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
export const GLYPH_PERSISTENT_CACHE_INFO = Object.freeze({
|
|
175
|
+
databaseName: DATABASE_NAME,
|
|
176
|
+
databaseVersion: DATABASE_VERSION,
|
|
177
|
+
schemaNamespace: GLYPH_CACHE_SCHEMA,
|
|
178
|
+
});
|
|
179
|
+
function readTotals(store) {
|
|
180
|
+
return request(store.get(TOTALS_KEY)).then((totals) => totals
|
|
181
|
+
? { ...totals, lastClearMs: Number.isFinite(totals.lastClearMs) ? totals.lastClearMs : Number.NEGATIVE_INFINITY }
|
|
182
|
+
: { key: TOTALS_KEY, entries: 0, bytes: 0, lastClearMs: Number.NEGATIVE_INFINITY });
|
|
183
|
+
}
|
|
184
|
+
function normalizeTotals(totals) {
|
|
185
|
+
totals.entries = Math.max(0, totals.entries);
|
|
186
|
+
totals.bytes = Math.max(0, totals.bytes);
|
|
187
|
+
return totals;
|
|
188
|
+
}
|
|
189
|
+
function request(value) {
|
|
190
|
+
return new Promise((resolve, reject) => {
|
|
191
|
+
value.onsuccess = () => resolve(value.result);
|
|
192
|
+
value.onerror = () => reject(value.error ?? new Error("IndexedDB request failed"));
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
function transactionDone(transaction) {
|
|
196
|
+
return new Promise((resolve, reject) => {
|
|
197
|
+
transaction.oncomplete = () => resolve();
|
|
198
|
+
transaction.onerror = () => reject(transaction.error ?? new Error("IndexedDB transaction failed"));
|
|
199
|
+
transaction.onabort = () => reject(transaction.error ?? new Error("IndexedDB transaction aborted"));
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
function iterateCursor(cursorRequest, visit) {
|
|
203
|
+
return new Promise((resolve, reject) => {
|
|
204
|
+
cursorRequest.onerror = () => reject(cursorRequest.error ?? new Error("IndexedDB cursor failed"));
|
|
205
|
+
cursorRequest.onsuccess = () => {
|
|
206
|
+
const cursor = cursorRequest.result;
|
|
207
|
+
if (!cursor || !visit(cursor)) {
|
|
208
|
+
resolve();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
cursor.continue();
|
|
212
|
+
};
|
|
213
|
+
});
|
|
214
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type DecodedImageValue<T> = {
|
|
2
|
+
value: T;
|
|
3
|
+
bytes: number;
|
|
4
|
+
};
|
|
5
|
+
export type ImageResourceLease<T> = {
|
|
6
|
+
value: T;
|
|
7
|
+
release(): void;
|
|
8
|
+
};
|
|
9
|
+
export declare class SessionImageResourceCache<T> {
|
|
10
|
+
private readonly budget;
|
|
11
|
+
private readonly entries;
|
|
12
|
+
private readonly inflight;
|
|
13
|
+
private clock;
|
|
14
|
+
private totalBytes;
|
|
15
|
+
private loads;
|
|
16
|
+
private hits;
|
|
17
|
+
private evictions;
|
|
18
|
+
constructor(budget: {
|
|
19
|
+
softBytes: number;
|
|
20
|
+
hardBytes: number;
|
|
21
|
+
dispose?: (value: T) => void;
|
|
22
|
+
});
|
|
23
|
+
acquire(key: string, loader: (signal: AbortSignal) => Promise<DecodedImageValue<T>>, signal?: AbortSignal): Promise<ImageResourceLease<T>>;
|
|
24
|
+
stats(): {
|
|
25
|
+
entries: number;
|
|
26
|
+
bytes: number;
|
|
27
|
+
pinned: number;
|
|
28
|
+
loads: number;
|
|
29
|
+
hits: number;
|
|
30
|
+
evictions: number;
|
|
31
|
+
};
|
|
32
|
+
keys(): string[];
|
|
33
|
+
clear(): void;
|
|
34
|
+
private load;
|
|
35
|
+
private lease;
|
|
36
|
+
private trimSoft;
|
|
37
|
+
private evictUntil;
|
|
38
|
+
private remove;
|
|
39
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
export class SessionImageResourceCache {
|
|
2
|
+
budget;
|
|
3
|
+
entries = new Map();
|
|
4
|
+
inflight = new Map();
|
|
5
|
+
clock = 0;
|
|
6
|
+
totalBytes = 0;
|
|
7
|
+
loads = 0;
|
|
8
|
+
hits = 0;
|
|
9
|
+
evictions = 0;
|
|
10
|
+
constructor(budget) {
|
|
11
|
+
if (!Number.isFinite(budget.softBytes) || !Number.isFinite(budget.hardBytes)
|
|
12
|
+
|| budget.softBytes < 0 || budget.hardBytes <= 0 || budget.softBytes > budget.hardBytes) {
|
|
13
|
+
throw new Error("invalid image cache byte budget");
|
|
14
|
+
}
|
|
15
|
+
this.budget = budget;
|
|
16
|
+
}
|
|
17
|
+
async acquire(key, loader, signal = new AbortController().signal) {
|
|
18
|
+
if (signal.aborted)
|
|
19
|
+
throw abortReason(signal);
|
|
20
|
+
const cached = this.entries.get(key);
|
|
21
|
+
if (cached) {
|
|
22
|
+
this.hits += 1;
|
|
23
|
+
cached.refs += 1;
|
|
24
|
+
cached.touched = ++this.clock;
|
|
25
|
+
return this.lease(cached);
|
|
26
|
+
}
|
|
27
|
+
let pending = this.inflight.get(key);
|
|
28
|
+
if (pending) {
|
|
29
|
+
this.hits += 1;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
this.loads += 1;
|
|
33
|
+
const controller = new AbortController();
|
|
34
|
+
const created = {
|
|
35
|
+
controller,
|
|
36
|
+
waiters: 0,
|
|
37
|
+
settled: false,
|
|
38
|
+
promise: undefined,
|
|
39
|
+
};
|
|
40
|
+
created.promise = Promise.resolve().then(() => this.load(key, () => loader(controller.signal), created));
|
|
41
|
+
pending = created;
|
|
42
|
+
this.inflight.set(key, pending);
|
|
43
|
+
void pending.promise.finally(() => {
|
|
44
|
+
pending.settled = true;
|
|
45
|
+
if (this.inflight.get(key) === pending)
|
|
46
|
+
this.inflight.delete(key);
|
|
47
|
+
}).catch(() => undefined);
|
|
48
|
+
}
|
|
49
|
+
pending.waiters += 1;
|
|
50
|
+
try {
|
|
51
|
+
const entry = await awaitWithAbort(pending.promise, signal);
|
|
52
|
+
entry.refs += 1;
|
|
53
|
+
entry.touched = ++this.clock;
|
|
54
|
+
return this.lease(entry);
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
pending.waiters = Math.max(0, pending.waiters - 1);
|
|
58
|
+
if (pending.waiters === 0 && !pending.settled) {
|
|
59
|
+
if (this.inflight.get(key) === pending)
|
|
60
|
+
this.inflight.delete(key);
|
|
61
|
+
pending.controller.abort(new DOMException("No resource waiters remain", "AbortError"));
|
|
62
|
+
}
|
|
63
|
+
this.trimSoft();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
stats() {
|
|
67
|
+
return {
|
|
68
|
+
entries: this.entries.size,
|
|
69
|
+
bytes: this.totalBytes,
|
|
70
|
+
pinned: [...this.entries.values()].filter((entry) => entry.refs > 0).length,
|
|
71
|
+
loads: this.loads,
|
|
72
|
+
hits: this.hits,
|
|
73
|
+
evictions: this.evictions,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
keys() {
|
|
77
|
+
return [...this.entries.entries()]
|
|
78
|
+
.sort(([, left], [, right]) => left.touched - right.touched)
|
|
79
|
+
.map(([key]) => key);
|
|
80
|
+
}
|
|
81
|
+
clear() {
|
|
82
|
+
for (const [key, entry] of this.entries) {
|
|
83
|
+
if (entry.refs === 0)
|
|
84
|
+
this.remove(key, entry);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async load(key, loader, owner) {
|
|
88
|
+
const decoded = await loader();
|
|
89
|
+
if (this.inflight.get(key) !== owner) {
|
|
90
|
+
this.budget.dispose?.(decoded.value);
|
|
91
|
+
throw abortReason(owner.controller.signal);
|
|
92
|
+
}
|
|
93
|
+
if (!Number.isFinite(decoded.bytes) || !Number.isInteger(decoded.bytes) || decoded.bytes < 0) {
|
|
94
|
+
throw new Error(`invalid decoded image byte size ${decoded.bytes}`);
|
|
95
|
+
}
|
|
96
|
+
if (decoded.bytes > this.budget.hardBytes) {
|
|
97
|
+
throw new Error(`decoded image exceeds hard byte budget: ${decoded.bytes} > ${this.budget.hardBytes}`);
|
|
98
|
+
}
|
|
99
|
+
this.evictUntil(this.budget.hardBytes - decoded.bytes);
|
|
100
|
+
if (this.totalBytes + decoded.bytes > this.budget.hardBytes) {
|
|
101
|
+
throw new Error(`decoded image cache hard byte budget is pinned`);
|
|
102
|
+
}
|
|
103
|
+
const entry = { value: decoded.value, bytes: decoded.bytes, refs: 0, touched: ++this.clock };
|
|
104
|
+
this.entries.set(key, entry);
|
|
105
|
+
this.totalBytes += decoded.bytes;
|
|
106
|
+
return entry;
|
|
107
|
+
}
|
|
108
|
+
lease(entry) {
|
|
109
|
+
let released = false;
|
|
110
|
+
return {
|
|
111
|
+
value: entry.value,
|
|
112
|
+
release: () => {
|
|
113
|
+
if (released)
|
|
114
|
+
return;
|
|
115
|
+
released = true;
|
|
116
|
+
entry.refs = Math.max(0, entry.refs - 1);
|
|
117
|
+
entry.touched = ++this.clock;
|
|
118
|
+
this.trimSoft();
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
trimSoft() {
|
|
123
|
+
this.evictUntil(this.budget.softBytes);
|
|
124
|
+
}
|
|
125
|
+
evictUntil(targetBytes) {
|
|
126
|
+
if (this.totalBytes <= targetBytes)
|
|
127
|
+
return;
|
|
128
|
+
const candidates = [...this.entries.entries()]
|
|
129
|
+
.filter(([, entry]) => entry.refs === 0)
|
|
130
|
+
.sort(([, left], [, right]) => left.touched - right.touched);
|
|
131
|
+
for (const [key, entry] of candidates) {
|
|
132
|
+
if (this.totalBytes <= targetBytes)
|
|
133
|
+
break;
|
|
134
|
+
this.remove(key, entry);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
remove(key, entry) {
|
|
138
|
+
if (!this.entries.delete(key))
|
|
139
|
+
return;
|
|
140
|
+
this.totalBytes -= entry.bytes;
|
|
141
|
+
this.evictions += 1;
|
|
142
|
+
this.budget.dispose?.(entry.value);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function awaitWithAbort(promise, signal) {
|
|
146
|
+
if (signal.aborted)
|
|
147
|
+
return Promise.reject(abortReason(signal));
|
|
148
|
+
return new Promise((resolve, reject) => {
|
|
149
|
+
const abort = () => reject(abortReason(signal));
|
|
150
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
151
|
+
void promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
function abortReason(signal) {
|
|
155
|
+
return signal.reason ?? new DOMException("The operation was aborted", "AbortError");
|
|
156
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type EmscriptenModule = {
|
|
2
|
+
HEAPU8: Uint8Array;
|
|
3
|
+
HEAPU32: Uint32Array;
|
|
4
|
+
_malloc(size: number): number;
|
|
5
|
+
_free(pointer: number): void;
|
|
6
|
+
ccall(name: string, returnType: "number" | null, argumentTypes: Array<"number" | "string">, arguments_: Array<number | string>): number | null;
|
|
7
|
+
};
|
|
8
|
+
export type EmscriptenModuleFactory = (options?: {
|
|
9
|
+
locateFile?: (path: string) => string;
|
|
10
|
+
printErr?: (...values: unknown[]) => void;
|
|
11
|
+
}) => Promise<EmscriptenModule>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type FontRequest = {
|
|
2
|
+
region: string;
|
|
3
|
+
family: string;
|
|
4
|
+
};
|
|
5
|
+
export type ProvidedFont = {
|
|
6
|
+
bytes: ArrayBuffer | Uint8Array;
|
|
7
|
+
};
|
|
8
|
+
export type FontContext = {
|
|
9
|
+
signal: AbortSignal;
|
|
10
|
+
};
|
|
11
|
+
/** Resolves a demanded font family through arbitrary caller-owned logic. */
|
|
12
|
+
export interface FontProvider {
|
|
13
|
+
provide(request: FontRequest, context: FontContext): Promise<ProvidedFont | null>;
|
|
14
|
+
}
|
|
15
|
+
export type FontProviderManagerOptions = {
|
|
16
|
+
provider: FontProvider;
|
|
17
|
+
concurrency?: number;
|
|
18
|
+
};
|
|
19
|
+
export type FontProviderStats = {
|
|
20
|
+
requested: number;
|
|
21
|
+
unique: number;
|
|
22
|
+
loaded: number;
|
|
23
|
+
bytes: number;
|
|
24
|
+
failures: number;
|
|
25
|
+
active: number;
|
|
26
|
+
peakActive: number;
|
|
27
|
+
resolveMs: number;
|
|
28
|
+
};
|
|
29
|
+
export declare class FontProviderManager {
|
|
30
|
+
private readonly provider;
|
|
31
|
+
private readonly concurrency;
|
|
32
|
+
private readonly counters;
|
|
33
|
+
constructor(options: FontProviderManagerOptions);
|
|
34
|
+
resolve(requests: readonly FontRequest[], signal?: AbortSignal): Promise<Map<string, ArrayBuffer>>;
|
|
35
|
+
stats(): FontProviderStats;
|
|
36
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
export class FontProviderManager {
|
|
2
|
+
provider;
|
|
3
|
+
concurrency;
|
|
4
|
+
counters = {
|
|
5
|
+
requested: 0,
|
|
6
|
+
unique: 0,
|
|
7
|
+
loaded: 0,
|
|
8
|
+
bytes: 0,
|
|
9
|
+
failures: 0,
|
|
10
|
+
active: 0,
|
|
11
|
+
peakActive: 0,
|
|
12
|
+
resolveMs: 0,
|
|
13
|
+
};
|
|
14
|
+
constructor(options) {
|
|
15
|
+
if (!options.provider || typeof options.provider.provide !== "function") {
|
|
16
|
+
throw new Error("font provider is required");
|
|
17
|
+
}
|
|
18
|
+
const concurrency = options.concurrency ?? 4;
|
|
19
|
+
if (!Number.isInteger(concurrency) || concurrency <= 0) {
|
|
20
|
+
throw new Error("font concurrency must be a positive integer");
|
|
21
|
+
}
|
|
22
|
+
this.provider = options.provider;
|
|
23
|
+
this.concurrency = concurrency;
|
|
24
|
+
}
|
|
25
|
+
async resolve(requests, signal = new AbortController().signal) {
|
|
26
|
+
const started = performance.now();
|
|
27
|
+
const unique = [...new Map(requests.map((request) => [
|
|
28
|
+
`${request.region}\0${request.family}`,
|
|
29
|
+
request,
|
|
30
|
+
])).values()];
|
|
31
|
+
this.counters.requested += requests.length;
|
|
32
|
+
this.counters.unique += unique.length;
|
|
33
|
+
const values = new Map();
|
|
34
|
+
let cursor = 0;
|
|
35
|
+
const worker = async () => {
|
|
36
|
+
while (cursor < unique.length) {
|
|
37
|
+
if (signal.aborted)
|
|
38
|
+
throw abortReason(signal);
|
|
39
|
+
const request = unique[cursor++];
|
|
40
|
+
this.counters.active += 1;
|
|
41
|
+
this.counters.peakActive = Math.max(this.counters.peakActive, this.counters.active);
|
|
42
|
+
let provided;
|
|
43
|
+
try {
|
|
44
|
+
provided = await this.provider.provide(request, { signal });
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
this.counters.failures += 1;
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
this.counters.active -= 1;
|
|
52
|
+
}
|
|
53
|
+
const bytes = provided?.bytes;
|
|
54
|
+
if (!(bytes instanceof ArrayBuffer) && !(bytes instanceof Uint8Array)) {
|
|
55
|
+
this.counters.failures += 1;
|
|
56
|
+
throw new Error(`font provider returned no bytes for ${request.region}:${request.family}`);
|
|
57
|
+
}
|
|
58
|
+
const snapshot = bytes instanceof ArrayBuffer
|
|
59
|
+
? bytes.slice(0)
|
|
60
|
+
: Uint8Array.from(bytes).buffer;
|
|
61
|
+
if (snapshot.byteLength === 0) {
|
|
62
|
+
this.counters.failures += 1;
|
|
63
|
+
throw new Error(`font provider returned an empty font for ${request.region}:${request.family}`);
|
|
64
|
+
}
|
|
65
|
+
this.counters.loaded += 1;
|
|
66
|
+
this.counters.bytes += snapshot.byteLength;
|
|
67
|
+
values.set(request.family, snapshot);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
try {
|
|
71
|
+
await Promise.all(Array.from({ length: Math.min(this.concurrency, unique.length) }, worker));
|
|
72
|
+
return values;
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
this.counters.resolveMs += performance.now() - started;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
stats() {
|
|
79
|
+
return { ...this.counters };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function abortReason(signal) {
|
|
83
|
+
return signal.reason ?? new DOMException("The operation was aborted", "AbortError");
|
|
84
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { GlyphPersistentCache } from "./cache/glyphPersistentCache.js";
|
|
2
|
+
import { RendererWorkerClient } from "./worker-client.js";
|
|
3
|
+
import type { RendererWorkerStats } from "./protocol.js";
|
|
4
|
+
import type { AtlasPageUpdate, AtlasStats } from "./types/atlas.js";
|
|
5
|
+
export type GlyphRequest = {
|
|
6
|
+
region: string;
|
|
7
|
+
family: string;
|
|
8
|
+
fontSourceHash: string;
|
|
9
|
+
char: string;
|
|
10
|
+
};
|
|
11
|
+
export type FontSource = {
|
|
12
|
+
region: string;
|
|
13
|
+
family: string;
|
|
14
|
+
sourceHash: string;
|
|
15
|
+
};
|
|
16
|
+
export type GlyphInfo = {
|
|
17
|
+
key: string;
|
|
18
|
+
resourceId: string;
|
|
19
|
+
glyphIndex: number;
|
|
20
|
+
page: number;
|
|
21
|
+
pageEpoch: number;
|
|
22
|
+
region: string;
|
|
23
|
+
family: string;
|
|
24
|
+
fontSourceHash: string;
|
|
25
|
+
char: string;
|
|
26
|
+
u0: number;
|
|
27
|
+
v0: number;
|
|
28
|
+
u1: number;
|
|
29
|
+
v1: number;
|
|
30
|
+
width: number;
|
|
31
|
+
height: number;
|
|
32
|
+
advance: number;
|
|
33
|
+
xOffset: number;
|
|
34
|
+
yOffset: number;
|
|
35
|
+
planeBearingX: number;
|
|
36
|
+
planeBearingY: number;
|
|
37
|
+
planeWidth: number;
|
|
38
|
+
planeHeight: number;
|
|
39
|
+
drawable: boolean;
|
|
40
|
+
};
|
|
41
|
+
export type SdfBackend = "edt" | "analytic";
|
|
42
|
+
export type PersistentCacheSelection = "memory-only" | "origin";
|
|
43
|
+
export type SdfAtlasPerf = {
|
|
44
|
+
backend: SdfBackend;
|
|
45
|
+
fontBatches: [];
|
|
46
|
+
totalGlyphMs: number;
|
|
47
|
+
totalFaceLoadMs: number;
|
|
48
|
+
totalGlyphCount: number;
|
|
49
|
+
totalPixelCount: number;
|
|
50
|
+
worker: RendererWorkerStats;
|
|
51
|
+
cache: {
|
|
52
|
+
hits: number;
|
|
53
|
+
misses: number;
|
|
54
|
+
generations: number;
|
|
55
|
+
bytes: number;
|
|
56
|
+
sessionHits: number;
|
|
57
|
+
persistentHits: number;
|
|
58
|
+
persistentMisses: number;
|
|
59
|
+
persistentWritesQueued: number;
|
|
60
|
+
pages: number;
|
|
61
|
+
pinnedPages: number;
|
|
62
|
+
pageEvictions: number;
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
export type SdfAtlas = {
|
|
66
|
+
width: number;
|
|
67
|
+
height: number;
|
|
68
|
+
depth: number;
|
|
69
|
+
baseSize: number;
|
|
70
|
+
spread: number;
|
|
71
|
+
glyphs: Map<string, GlyphInfo>;
|
|
72
|
+
missing: string[];
|
|
73
|
+
backend: "freetype-wasm";
|
|
74
|
+
sdfBackend: SdfBackend;
|
|
75
|
+
contractId: string;
|
|
76
|
+
fontEngineFingerprint: string;
|
|
77
|
+
perf: SdfAtlasPerf;
|
|
78
|
+
pageUpdates(revisions: ReadonlyMap<number, number>): Promise<AtlasPageUpdate[]>;
|
|
79
|
+
release(): Promise<void>;
|
|
80
|
+
};
|
|
81
|
+
export declare function glyphKey(region: string, fontSourceHash: string, family: string, char: string): string;
|
|
82
|
+
export declare function buildSdfAtlas(fontSources: FontSource[], requests: GlyphRequest[], options: {
|
|
83
|
+
worker: RendererWorkerClient;
|
|
84
|
+
persistence?: PersistentCacheSelection;
|
|
85
|
+
persistentCache?: GlyphPersistentCache;
|
|
86
|
+
}, supersample?: number, backend?: SdfBackend): Promise<SdfAtlas>;
|
|
87
|
+
export declare function getPersistentGlyphCache(): GlyphPersistentCache;
|
|
88
|
+
export declare function getSessionAtlasStats(): Array<{
|
|
89
|
+
contractId: string;
|
|
90
|
+
stats: AtlasStats;
|
|
91
|
+
}>;
|
|
92
|
+
export declare function disposeWorkerAtlasSessions(worker: RendererWorkerClient): void;
|