@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.
Files changed (130) hide show
  1. package/LICENSE +235 -0
  2. package/LICENSE-EXCEPTION +32 -0
  3. package/NOTICE +11 -0
  4. package/README.en.md +456 -0
  5. package/README.md +458 -0
  6. package/dist/allium_renderer_wasm.js +14 -0
  7. package/dist/allium_renderer_wasm.wasm +0 -0
  8. package/dist/authoring.d.ts +14 -0
  9. package/dist/authoring.js +34 -0
  10. package/dist/cache/glyphPersistentCache.d.ts +145 -0
  11. package/dist/cache/glyphPersistentCache.js +346 -0
  12. package/dist/cache/indexedDbGlyphRecordStore.d.ts +26 -0
  13. package/dist/cache/indexedDbGlyphRecordStore.js +214 -0
  14. package/dist/cache/sessionImageResourceCache.d.ts +39 -0
  15. package/dist/cache/sessionImageResourceCache.js +156 -0
  16. package/dist/emscripten.d.ts +11 -0
  17. package/dist/emscripten.js +1 -0
  18. package/dist/fontProvider.d.ts +36 -0
  19. package/dist/fontProvider.js +84 -0
  20. package/dist/fontSdfAtlas.d.ts +92 -0
  21. package/dist/fontSdfAtlas.js +296 -0
  22. package/dist/gpu/browserSemanticResources.d.ts +64 -0
  23. package/dist/gpu/browserSemanticResources.js +232 -0
  24. package/dist/gpu/generalTextRenderPlacement.d.ts +8 -0
  25. package/dist/gpu/generalTextRenderPlacement.js +131 -0
  26. package/dist/gpu/previewTransformTextureLayout.d.ts +1 -0
  27. package/dist/gpu/previewTransformTextureLayout.js +12 -0
  28. package/dist/gpu/semanticCommandGeometry.d.ts +21 -0
  29. package/dist/gpu/semanticCommandGeometry.js +226 -0
  30. package/dist/gpu/semanticCommandPlanner.d.ts +131 -0
  31. package/dist/gpu/semanticCommandPlanner.js +203 -0
  32. package/dist/gpu/semanticTextGlyphBridge.d.ts +3 -0
  33. package/dist/gpu/semanticTextGlyphBridge.js +22 -0
  34. package/dist/gpu/semanticWebglSceneRenderer.d.ts +70 -0
  35. package/dist/gpu/semanticWebglSceneRenderer.js +211 -0
  36. package/dist/gpu/slotRanges.d.ts +5 -0
  37. package/dist/gpu/slotRanges.js +14 -0
  38. package/dist/gpu/webglSdfAtlasTexture.d.ts +17 -0
  39. package/dist/gpu/webglSdfAtlasTexture.js +72 -0
  40. package/dist/gpu/webglSdfGlyphPipeline.d.ts +26 -0
  41. package/dist/gpu/webglSdfGlyphPipeline.js +267 -0
  42. package/dist/gpu/webglSemanticCommandExecutor.d.ts +79 -0
  43. package/dist/gpu/webglSemanticCommandExecutor.js +686 -0
  44. package/dist/index.d.ts +18 -0
  45. package/dist/index.js +8 -0
  46. package/dist/interaction/numericTextRegions.d.ts +46 -0
  47. package/dist/interaction/numericTextRegions.js +44 -0
  48. package/dist/localizationProvider.d.ts +33 -0
  49. package/dist/localizationProvider.js +74 -0
  50. package/dist/originPrebuiltSdfAtlasPackage.d.ts +39 -0
  51. package/dist/originPrebuiltSdfAtlasPackage.js +316 -0
  52. package/dist/prebuiltSdfAtlas.d.ts +36 -0
  53. package/dist/prebuiltSdfAtlas.js +261 -0
  54. package/dist/protocol.d.ts +456 -0
  55. package/dist/protocol.js +1 -0
  56. package/dist/renderer.d.ts +218 -0
  57. package/dist/renderer.js +641 -0
  58. package/dist/resourceProvider.d.ts +24 -0
  59. package/dist/resourceProvider.js +41 -0
  60. package/dist/telemetry/rendererTelemetry.d.ts +158 -0
  61. package/dist/telemetry/rendererTelemetry.js +232 -0
  62. package/dist/third-party/freetype/FTL.txt +169 -0
  63. package/dist/types/atlas.d.ts +90 -0
  64. package/dist/types/atlas.js +1 -0
  65. package/dist/types/authoring.d.ts +81 -0
  66. package/dist/types/authoring.js +1 -0
  67. package/dist/types/core.d.ts +128 -0
  68. package/dist/types/core.js +1 -0
  69. package/dist/types/freeType.d.ts +54 -0
  70. package/dist/types/freeType.js +13 -0
  71. package/dist/types/glyph.d.ts +33 -0
  72. package/dist/types/glyph.js +1 -0
  73. package/dist/types/layout.d.ts +25 -0
  74. package/dist/types/layout.js +1 -0
  75. package/dist/types/text.d.ts +28 -0
  76. package/dist/types/text.js +1 -0
  77. package/dist/worker/glyphWorkScheduler.d.ts +29 -0
  78. package/dist/worker/glyphWorkScheduler.js +107 -0
  79. package/dist/worker-client.d.ts +114 -0
  80. package/dist/worker-client.js +413 -0
  81. package/dist/worker.d.ts +1 -0
  82. package/dist/worker.js +708 -0
  83. package/package.json +46 -0
  84. package/src/atlas.rs +652 -0
  85. package/src/authoring.ts +42 -0
  86. package/src/authoring_runtime.rs +303 -0
  87. package/src/cache/glyphPersistentCache.ts +463 -0
  88. package/src/cache/indexedDbGlyphRecordStore.ts +238 -0
  89. package/src/cache/sessionImageResourceCache.ts +186 -0
  90. package/src/edt.rs +97 -0
  91. package/src/emscripten.ts +17 -0
  92. package/src/fontProvider.ts +127 -0
  93. package/src/fontSdfAtlas.ts +431 -0
  94. package/src/geometry.rs +149 -0
  95. package/src/glyph_plan.rs +226 -0
  96. package/src/gpu/browserSemanticResources.ts +294 -0
  97. package/src/gpu/generalTextRenderPlacement.ts +175 -0
  98. package/src/gpu/previewTransformTextureLayout.ts +12 -0
  99. package/src/gpu/semanticCommandGeometry.ts +260 -0
  100. package/src/gpu/semanticCommandPlanner.ts +280 -0
  101. package/src/gpu/semanticTextGlyphBridge.ts +35 -0
  102. package/src/gpu/semanticWebglSceneRenderer.ts +261 -0
  103. package/src/gpu/slotRanges.ts +15 -0
  104. package/src/gpu/webglSdfAtlasTexture.ts +69 -0
  105. package/src/gpu/webglSdfGlyphPipeline.ts +299 -0
  106. package/src/gpu/webglSemanticCommandExecutor.ts +739 -0
  107. package/src/index.ts +84 -0
  108. package/src/interaction/numericTextRegions.ts +80 -0
  109. package/src/layout.rs +1882 -0
  110. package/src/lib.rs +1443 -0
  111. package/src/localizationProvider.ts +113 -0
  112. package/src/main.rs +55 -0
  113. package/src/masterdata_runtime.rs +496 -0
  114. package/src/originPrebuiltSdfAtlasPackage.ts +403 -0
  115. package/src/prebuiltSdfAtlas.ts +306 -0
  116. package/src/protocol.ts +150 -0
  117. package/src/renderer.ts +792 -0
  118. package/src/resourceProvider.ts +69 -0
  119. package/src/scene.rs +665 -0
  120. package/src/telemetry/rendererTelemetry.ts +358 -0
  121. package/src/types/atlas.ts +80 -0
  122. package/src/types/authoring.ts +85 -0
  123. package/src/types/core.ts +110 -0
  124. package/src/types/freeType.ts +63 -0
  125. package/src/types/glyph.ts +33 -0
  126. package/src/types/layout.ts +28 -0
  127. package/src/types/text.ts +28 -0
  128. package/src/worker/glyphWorkScheduler.ts +130 -0
  129. package/src/worker-client.ts +484 -0
  130. package/src/worker.ts +895 -0
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@empty-sekai/sekai-custom-profile-sdk",
3
+ "version": "0.3.0",
4
+ "description": "Unofficial Project SEKAI custom profile SDK with authoring, native-compatible scenes, WebGL2 rendering, and WASM.",
5
+ "type": "module",
6
+ "license": "AGPL-3.0-only",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/empty-sekai/sekai-custom-profile-sdk.git"
10
+ },
11
+ "files": [
12
+ "dist/",
13
+ "src/",
14
+ "LICENSE",
15
+ "LICENSE-EXCEPTION",
16
+ "NOTICE",
17
+ "README.md",
18
+ "README.en.md"
19
+ ],
20
+ "main": "./dist/index.js",
21
+ "module": "./dist/index.js",
22
+ "types": "./dist/index.d.ts",
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "import": "./dist/index.js"
27
+ }
28
+ },
29
+ "scripts": {
30
+ "build:wasm": "bash ./build.sh",
31
+ "build:ts": "tsc -p tsconfig.json && node ./scripts/copy-artifacts.mjs",
32
+ "build": "npm run build:wasm && npm run build:ts",
33
+ "typecheck": "tsc -p tsconfig.json --noEmit",
34
+ "test:gates": "node --experimental-strip-types --test scripts/test/*.test.mjs",
35
+ "audit:public": "node scripts/audit-public-package.mjs",
36
+ "verify:wasm:runtime": "node scripts/verify-wasm-runtime.mjs",
37
+ "measure:wasm:size": "node scripts/measure-wasm-size.mjs",
38
+ "demo:serve": "node scripts/serve-demo.mjs"
39
+ },
40
+ "devDependencies": {
41
+ "typescript": "^5.6.0"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ }
46
+ }
package/src/atlas.rs ADDED
@@ -0,0 +1,652 @@
1
+ use std::cell::RefCell;
2
+ use std::collections::{BTreeMap, BTreeSet};
3
+
4
+ use base64::Engine;
5
+ use serde::{Deserialize, Serialize};
6
+
7
+ #[derive(Clone, Copy, Debug, Deserialize, Serialize)]
8
+ #[serde(default, rename_all = "camelCase")]
9
+ pub struct AtlasConfig {
10
+ pub page_width: usize,
11
+ pub page_height: usize,
12
+ pub soft_pages: usize,
13
+ pub hard_pages: usize,
14
+ }
15
+
16
+ impl Default for AtlasConfig {
17
+ fn default() -> Self {
18
+ Self {
19
+ page_width: 2048,
20
+ page_height: 2048,
21
+ soft_pages: 4,
22
+ hard_pages: 6,
23
+ }
24
+ }
25
+ }
26
+
27
+ #[derive(Clone, Debug)]
28
+ pub struct AtlasBitmap {
29
+ pub key: String,
30
+ pub width: usize,
31
+ pub height: usize,
32
+ pub pixels: Vec<u8>,
33
+ }
34
+
35
+ #[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
36
+ #[serde(rename_all = "camelCase")]
37
+ pub struct PixelRect {
38
+ pub x: usize,
39
+ pub y: usize,
40
+ pub width: usize,
41
+ pub height: usize,
42
+ }
43
+
44
+ #[derive(Clone, Debug, PartialEq, Serialize)]
45
+ #[serde(rename_all = "camelCase")]
46
+ pub struct AtlasPlacement {
47
+ pub page: usize,
48
+ pub page_epoch: u64,
49
+ pub pixel_rect: PixelRect,
50
+ pub u0: f32,
51
+ pub v0: f32,
52
+ pub u1: f32,
53
+ pub v1: f32,
54
+ }
55
+
56
+ #[derive(Clone, Debug, PartialEq, Serialize)]
57
+ #[serde(rename_all = "camelCase")]
58
+ pub struct AtlasPageUpdateMeta {
59
+ pub page: usize,
60
+ pub page_width: usize,
61
+ pub page_epoch: u64,
62
+ pub revision: u64,
63
+ pub full_upload: bool,
64
+ pub dirty_rects: Vec<PixelRect>,
65
+ }
66
+
67
+ #[derive(Clone, Debug, PartialEq, Serialize)]
68
+ #[serde(rename_all = "camelCase")]
69
+ pub struct AtlasStats {
70
+ pub pages: usize,
71
+ pub pinned_pages: usize,
72
+ pub glyphs: usize,
73
+ pub atlas_bytes: usize,
74
+ pub evictions: u64,
75
+ pub page_allocations: u64,
76
+ pub hard_budget_bytes: usize,
77
+ }
78
+
79
+ #[derive(Debug, Deserialize)]
80
+ #[serde(rename_all = "camelCase")]
81
+ pub struct AtlasRasterRecord {
82
+ pub key: String,
83
+ pub width: usize,
84
+ pub height: usize,
85
+ pub pixels_base64: String,
86
+ }
87
+
88
+ #[derive(Debug, Deserialize)]
89
+ #[serde(rename_all = "camelCase")]
90
+ pub struct AtlasResolveRequest {
91
+ #[serde(default)]
92
+ pub keys: Vec<String>,
93
+ pub records: Vec<AtlasRasterRecord>,
94
+ }
95
+
96
+ #[derive(Debug, Serialize)]
97
+ #[serde(rename_all = "camelCase")]
98
+ pub struct AtlasResolvedGlyph {
99
+ pub key: String,
100
+ pub placement: AtlasPlacement,
101
+ }
102
+
103
+ #[derive(Debug, Serialize)]
104
+ #[serde(rename_all = "camelCase")]
105
+ pub struct AtlasResolveResponse {
106
+ pub lease: Option<u32>,
107
+ pub placements: Vec<AtlasResolvedGlyph>,
108
+ pub missing_keys: Vec<String>,
109
+ pub stats: AtlasStats,
110
+ }
111
+
112
+ #[derive(Debug, Deserialize)]
113
+ #[serde(rename_all = "camelCase")]
114
+ pub struct AtlasRevision {
115
+ pub page: usize,
116
+ pub revision: u64,
117
+ }
118
+
119
+ #[derive(Debug, Deserialize)]
120
+ #[serde(rename_all = "camelCase")]
121
+ pub struct AtlasPagesRequest {
122
+ pub revisions: Vec<AtlasRevision>,
123
+ }
124
+
125
+ #[derive(Debug, PartialEq)]
126
+ pub enum AtlasError {
127
+ InvalidConfig,
128
+ InvalidBitmap(String),
129
+ MissingGlyph(String),
130
+ MemoryBudgetExceeded {
131
+ pages: usize,
132
+ pinned_pages: usize,
133
+ hard_budget_bytes: usize,
134
+ },
135
+ }
136
+
137
+ impl std::fmt::Display for AtlasError {
138
+ fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139
+ match self {
140
+ Self::InvalidConfig => write!(formatter, "invalid atlas dimensions or page budget"),
141
+ Self::InvalidBitmap(key) => write!(formatter, "glyph {key} does not fit the atlas page or has an invalid R8 payload"),
142
+ Self::MissingGlyph(key) => write!(formatter, "glyph {key} is not placed in the atlas"),
143
+ Self::MemoryBudgetExceeded { pages, pinned_pages, hard_budget_bytes } => write!(
144
+ formatter,
145
+ "MEMORY_BUDGET_EXCEEDED: all {pages} atlas pages are pinned ({pinned_pages}) at the {hard_budget_bytes}-byte hard budget"
146
+ ),
147
+ }
148
+ }
149
+ }
150
+
151
+ struct PageChange {
152
+ revision: u64,
153
+ rect: PixelRect,
154
+ }
155
+
156
+ struct AtlasPage {
157
+ epoch: u64,
158
+ revision: u64,
159
+ generation_start_revision: u64,
160
+ pixels: Vec<u8>,
161
+ x: usize,
162
+ y: usize,
163
+ row_height: usize,
164
+ pins: usize,
165
+ last_used: u64,
166
+ glyph_keys: BTreeSet<String>,
167
+ changes: Vec<PageChange>,
168
+ }
169
+
170
+ pub struct AtlasSession {
171
+ config: AtlasConfig,
172
+ pages: Vec<AtlasPage>,
173
+ placements: BTreeMap<String, AtlasPlacement>,
174
+ leases: BTreeMap<u32, Vec<usize>>,
175
+ next_lease: u32,
176
+ clock: u64,
177
+ revision: u64,
178
+ evictions: u64,
179
+ page_allocations: u64,
180
+ }
181
+
182
+ impl AtlasSession {
183
+ pub fn new(config: AtlasConfig) -> Result<Self, AtlasError> {
184
+ if config.page_width < 3
185
+ || config.page_height < 3
186
+ || config.soft_pages < 1
187
+ || config.hard_pages < config.soft_pages
188
+ || config
189
+ .page_width
190
+ .checked_mul(config.page_height)
191
+ .and_then(|bytes| bytes.checked_mul(config.hard_pages))
192
+ .is_none()
193
+ {
194
+ return Err(AtlasError::InvalidConfig);
195
+ }
196
+ Ok(Self {
197
+ config,
198
+ pages: Vec::new(),
199
+ placements: BTreeMap::new(),
200
+ leases: BTreeMap::new(),
201
+ next_lease: 1,
202
+ clock: 0,
203
+ revision: 0,
204
+ evictions: 0,
205
+ page_allocations: 0,
206
+ })
207
+ }
208
+
209
+ pub fn put(&mut self, glyph: AtlasBitmap) -> Result<AtlasPlacement, AtlasError> {
210
+ if let Some(existing) = self.placements.get(&glyph.key).cloned() {
211
+ self.touch(existing.page);
212
+ return Ok(existing);
213
+ }
214
+ if glyph.width < 1
215
+ || glyph.height < 1
216
+ || glyph.width + 2 > self.config.page_width
217
+ || glyph.height + 2 > self.config.page_height
218
+ || glyph.pixels.len() != glyph.width * glyph.height
219
+ {
220
+ return Err(AtlasError::InvalidBitmap(glyph.key));
221
+ }
222
+ let mut page_index = self
223
+ .pages
224
+ .iter()
225
+ .position(|page| can_fit(page, &glyph, self.config));
226
+ if page_index.is_none() {
227
+ page_index = Some(self.allocate_or_recycle_page()?);
228
+ }
229
+ let page_index = page_index.expect("atlas page index");
230
+ let (x, y, epoch, revision) = {
231
+ let page = &mut self.pages[page_index];
232
+ prepare_row(page, glyph.width, self.config.page_width);
233
+ let x = page.x;
234
+ let y = page.y;
235
+ for row in 0..glyph.height {
236
+ let source = row * glyph.width;
237
+ let destination = (y + row) * self.config.page_width + x;
238
+ page.pixels[destination..destination + glyph.width]
239
+ .copy_from_slice(&glyph.pixels[source..source + glyph.width]);
240
+ }
241
+ page.x += glyph.width + 1;
242
+ page.row_height = page.row_height.max(glyph.height);
243
+ page.glyph_keys.insert(glyph.key.clone());
244
+ self.revision += 1;
245
+ page.revision = self.revision;
246
+ page.changes.push(PageChange {
247
+ revision: page.revision,
248
+ rect: PixelRect {
249
+ x,
250
+ y,
251
+ width: glyph.width,
252
+ height: glyph.height,
253
+ },
254
+ });
255
+ (x, y, page.epoch, page.revision)
256
+ };
257
+ self.touch(page_index);
258
+ let placement = AtlasPlacement {
259
+ page: page_index,
260
+ page_epoch: epoch,
261
+ pixel_rect: PixelRect {
262
+ x,
263
+ y,
264
+ width: glyph.width,
265
+ height: glyph.height,
266
+ },
267
+ u0: x as f32 / self.config.page_width as f32,
268
+ v0: y as f32 / self.config.page_height as f32,
269
+ u1: (x + glyph.width) as f32 / self.config.page_width as f32,
270
+ v1: (y + glyph.height) as f32 / self.config.page_height as f32,
271
+ };
272
+ debug_assert_eq!(self.pages[page_index].revision, revision);
273
+ self.placements.insert(glyph.key, placement.clone());
274
+ Ok(placement)
275
+ }
276
+
277
+ pub fn placement(&self, key: &str) -> Option<AtlasPlacement> {
278
+ self.placements.get(key).cloned()
279
+ }
280
+
281
+ pub fn acquire(&mut self, keys: &[String]) -> Result<u32, AtlasError> {
282
+ let mut pages = keys
283
+ .iter()
284
+ .map(|key| {
285
+ self.placements
286
+ .get(key)
287
+ .map(|placement| placement.page)
288
+ .ok_or_else(|| AtlasError::MissingGlyph(key.clone()))
289
+ })
290
+ .collect::<Result<Vec<_>, _>>()?;
291
+ pages.sort_unstable();
292
+ pages.dedup();
293
+ for page_index in &pages {
294
+ self.pages[*page_index].pins += 1;
295
+ self.touch(*page_index);
296
+ }
297
+ let lease = self.next_lease;
298
+ self.next_lease = self.next_lease.checked_add(1).unwrap_or(1);
299
+ self.leases.insert(lease, pages);
300
+ Ok(lease)
301
+ }
302
+
303
+ pub fn release(&mut self, lease: u32) -> bool {
304
+ let Some(pages) = self.leases.remove(&lease) else {
305
+ return false;
306
+ };
307
+ for page_index in pages {
308
+ self.pages[page_index].pins = self.pages[page_index].pins.saturating_sub(1);
309
+ self.touch(page_index);
310
+ }
311
+ true
312
+ }
313
+
314
+ pub fn pages_since(&self, revisions: &BTreeMap<usize, u64>) -> Vec<AtlasPageUpdateMeta> {
315
+ self.pages
316
+ .iter()
317
+ .enumerate()
318
+ .filter_map(|(page_index, page)| {
319
+ let known = revisions.get(&page_index).copied().unwrap_or(0);
320
+ if known >= page.revision {
321
+ return None;
322
+ }
323
+ let full_upload = known < page.generation_start_revision;
324
+ Some(AtlasPageUpdateMeta {
325
+ page: page_index,
326
+ page_width: self.config.page_width,
327
+ page_epoch: page.epoch,
328
+ revision: page.revision,
329
+ full_upload,
330
+ dirty_rects: if full_upload {
331
+ Vec::new()
332
+ } else {
333
+ page.changes
334
+ .iter()
335
+ .filter(|change| change.revision > known)
336
+ .map(|change| change.rect)
337
+ .collect()
338
+ },
339
+ })
340
+ })
341
+ .collect()
342
+ }
343
+
344
+ pub fn page_pixels(&self, page: usize) -> Option<&[u8]> {
345
+ self.pages.get(page).map(|page| page.pixels.as_slice())
346
+ }
347
+
348
+ pub fn stats(&self) -> AtlasStats {
349
+ let page_bytes = self.config.page_width * self.config.page_height;
350
+ AtlasStats {
351
+ pages: self.pages.len(),
352
+ pinned_pages: self.pages.iter().filter(|page| page.pins > 0).count(),
353
+ glyphs: self.placements.len(),
354
+ atlas_bytes: self.pages.len() * page_bytes,
355
+ evictions: self.evictions,
356
+ page_allocations: self.page_allocations,
357
+ hard_budget_bytes: self.config.hard_pages * page_bytes,
358
+ }
359
+ }
360
+
361
+ fn allocate_or_recycle_page(&mut self) -> Result<usize, AtlasError> {
362
+ if self.pages.len() < self.config.hard_pages {
363
+ self.revision += 1;
364
+ let page_index = self.pages.len();
365
+ self.pages.push(AtlasPage {
366
+ epoch: 1,
367
+ revision: self.revision,
368
+ generation_start_revision: self.revision,
369
+ pixels: vec![0; self.config.page_width * self.config.page_height],
370
+ x: 1,
371
+ y: 1,
372
+ row_height: 0,
373
+ pins: 0,
374
+ last_used: 0,
375
+ glyph_keys: BTreeSet::new(),
376
+ changes: Vec::new(),
377
+ });
378
+ self.page_allocations += 1;
379
+ self.touch(page_index);
380
+ return Ok(page_index);
381
+ }
382
+ let Some(page_index) = self
383
+ .pages
384
+ .iter()
385
+ .enumerate()
386
+ .filter(|(_, page)| page.pins == 0)
387
+ .min_by_key(|(index, page)| (page.last_used, *index))
388
+ .map(|(index, _)| index)
389
+ else {
390
+ let stats = self.stats();
391
+ return Err(AtlasError::MemoryBudgetExceeded {
392
+ pages: stats.pages,
393
+ pinned_pages: stats.pinned_pages,
394
+ hard_budget_bytes: stats.hard_budget_bytes,
395
+ });
396
+ };
397
+ let keys = std::mem::take(&mut self.pages[page_index].glyph_keys);
398
+ for key in keys {
399
+ self.placements.remove(&key);
400
+ }
401
+ self.revision += 1;
402
+ let page = &mut self.pages[page_index];
403
+ page.epoch += 1;
404
+ page.revision = self.revision;
405
+ page.generation_start_revision = self.revision;
406
+ page.pixels.fill(0);
407
+ page.x = 1;
408
+ page.y = 1;
409
+ page.row_height = 0;
410
+ page.changes.clear();
411
+ self.evictions += 1;
412
+ self.touch(page_index);
413
+ Ok(page_index)
414
+ }
415
+
416
+ fn touch(&mut self, page: usize) {
417
+ self.clock += 1;
418
+ self.pages[page].last_used = self.clock;
419
+ }
420
+ }
421
+
422
+ fn can_fit(page: &AtlasPage, glyph: &AtlasBitmap, config: AtlasConfig) -> bool {
423
+ let mut x = page.x;
424
+ let mut y = page.y;
425
+ let mut row_height = page.row_height;
426
+ if x > 1 && x + glyph.width + 1 > config.page_width {
427
+ x = 1;
428
+ y += row_height + 1;
429
+ row_height = 0;
430
+ }
431
+ let _ = x;
432
+ y + row_height.max(glyph.height) < config.page_height
433
+ }
434
+
435
+ fn prepare_row(page: &mut AtlasPage, glyph_width: usize, page_width: usize) {
436
+ if page.x > 1 && page.x + glyph_width + 1 > page_width {
437
+ page.x = 1;
438
+ page.y += page.row_height + 1;
439
+ page.row_height = 0;
440
+ }
441
+ }
442
+
443
+ thread_local! {
444
+ static ATLASES: RefCell<BTreeMap<u32, AtlasSession>> = RefCell::new(BTreeMap::new());
445
+ static NEXT_HANDLE: RefCell<u32> = const { RefCell::new(1) };
446
+ }
447
+
448
+ pub fn create(config: AtlasConfig) -> Result<(u32, AtlasStats), String> {
449
+ let session = AtlasSession::new(config).map_err(|error| error.to_string())?;
450
+ let handle = NEXT_HANDLE.with(|next| {
451
+ let mut next = next.borrow_mut();
452
+ let value = *next;
453
+ *next = next.checked_add(1).unwrap_or(1);
454
+ value
455
+ });
456
+ let stats = session.stats();
457
+ ATLASES.with(|atlases| atlases.borrow_mut().insert(handle, session));
458
+ Ok((handle, stats))
459
+ }
460
+
461
+ pub fn with_session<T>(
462
+ handle: u32,
463
+ operation: impl FnOnce(&mut AtlasSession) -> Result<T, String>,
464
+ ) -> Result<T, String> {
465
+ ATLASES.with(|atlases| {
466
+ let mut atlases = atlases.borrow_mut();
467
+ let session = atlases
468
+ .get_mut(&handle)
469
+ .ok_or_else(|| format!("unknown atlas handle {handle}"))?;
470
+ operation(session)
471
+ })
472
+ }
473
+
474
+ pub fn resolve(handle: u32, request: AtlasResolveRequest) -> Result<AtlasResolveResponse, String> {
475
+ with_session(handle, |session| {
476
+ let mut keys = request.keys;
477
+ for record in request.records {
478
+ let key = record.key;
479
+ let placement = if let Some(existing) = session.placement(&key) {
480
+ existing
481
+ } else {
482
+ let pixels = base64::engine::general_purpose::STANDARD
483
+ .decode(record.pixels_base64)
484
+ .map_err(|error| format!("invalid atlas R8 payload for {key}: {error}"))?;
485
+ session
486
+ .put(AtlasBitmap {
487
+ key: key.clone(),
488
+ width: record.width,
489
+ height: record.height,
490
+ pixels,
491
+ })
492
+ .map_err(|error| error.to_string())?
493
+ };
494
+ let _ = placement;
495
+ keys.push(key);
496
+ }
497
+ keys.sort();
498
+ keys.dedup();
499
+ let mut placed_keys = Vec::new();
500
+ let mut placements = Vec::new();
501
+ let mut missing_keys = Vec::new();
502
+ for key in keys {
503
+ if let Some(placement) = session.placement(&key) {
504
+ placed_keys.push(key.clone());
505
+ placements.push(AtlasResolvedGlyph { key, placement });
506
+ } else {
507
+ missing_keys.push(key);
508
+ }
509
+ }
510
+ let lease = if placed_keys.is_empty() {
511
+ None
512
+ } else {
513
+ Some(
514
+ session
515
+ .acquire(&placed_keys)
516
+ .map_err(|error| error.to_string())?,
517
+ )
518
+ };
519
+ Ok(AtlasResolveResponse {
520
+ lease,
521
+ placements,
522
+ missing_keys,
523
+ stats: session.stats(),
524
+ })
525
+ })
526
+ }
527
+
528
+ pub fn pages_since(
529
+ handle: u32,
530
+ request: AtlasPagesRequest,
531
+ ) -> Result<Vec<AtlasPageUpdateMeta>, String> {
532
+ let revisions = request
533
+ .revisions
534
+ .into_iter()
535
+ .map(|entry| (entry.page, entry.revision))
536
+ .collect();
537
+ with_session(handle, |session| Ok(session.pages_since(&revisions)))
538
+ }
539
+
540
+ pub fn release(handle: u32, lease: u32) -> Result<bool, String> {
541
+ with_session(handle, |session| Ok(session.release(lease)))
542
+ }
543
+
544
+ pub fn page_pixels(handle: u32, page: usize) -> Result<(*const u8, usize), String> {
545
+ with_session(handle, |session| {
546
+ let pixels = session
547
+ .page_pixels(page)
548
+ .ok_or_else(|| format!("unknown atlas page {page}"))?;
549
+ Ok((pixels.as_ptr(), pixels.len()))
550
+ })
551
+ }
552
+
553
+ pub fn destroy(handle: u32) -> bool {
554
+ ATLASES.with(|atlases| atlases.borrow_mut().remove(&handle).is_some())
555
+ }
556
+
557
+ #[cfg(test)]
558
+ mod tests {
559
+ use std::collections::BTreeMap;
560
+
561
+ use super::{AtlasBitmap, AtlasConfig, AtlasError, AtlasSession, PixelRect};
562
+
563
+ fn glyph(key: &str, value: u8) -> AtlasBitmap {
564
+ AtlasBitmap {
565
+ key: key.to_string(),
566
+ width: 5,
567
+ height: 5,
568
+ pixels: vec![value; 25],
569
+ }
570
+ }
571
+
572
+ #[test]
573
+ fn placement_and_dirty_rects_are_owned_by_the_atlas_session() {
574
+ let mut atlas = AtlasSession::new(AtlasConfig {
575
+ page_width: 8,
576
+ page_height: 8,
577
+ soft_pages: 1,
578
+ hard_pages: 2,
579
+ })
580
+ .unwrap();
581
+ let placement = atlas.put(glyph("alpha", 200)).unwrap();
582
+ assert_eq!(placement.page, 0);
583
+ assert_eq!((placement.pixel_rect.x, placement.pixel_rect.y), (1, 1));
584
+ assert_eq!((placement.u0, placement.v0), (0.125, 0.125));
585
+ let updates = atlas.pages_since(&Default::default());
586
+ assert_eq!(updates.len(), 1);
587
+ assert!(updates[0].full_upload);
588
+ assert_eq!(atlas.page_pixels(0).unwrap().len(), 64);
589
+
590
+ let mut dirty = AtlasSession::new(AtlasConfig {
591
+ page_width: 8,
592
+ page_height: 8,
593
+ soft_pages: 1,
594
+ hard_pages: 1,
595
+ })
596
+ .unwrap();
597
+ dirty
598
+ .put(AtlasBitmap {
599
+ key: "first".to_string(),
600
+ width: 2,
601
+ height: 2,
602
+ pixels: vec![7; 4],
603
+ })
604
+ .unwrap();
605
+ let known = dirty.pages_since(&Default::default())[0].revision;
606
+ dirty
607
+ .put(AtlasBitmap {
608
+ key: "second".to_string(),
609
+ width: 1,
610
+ height: 1,
611
+ pixels: vec![9],
612
+ })
613
+ .unwrap();
614
+ let updates = dirty.pages_since(&BTreeMap::from([(0, known)]));
615
+ assert_eq!(updates.len(), 1);
616
+ assert!(!updates[0].full_upload);
617
+ assert_eq!(
618
+ updates[0].dirty_rects,
619
+ vec![PixelRect {
620
+ x: 4,
621
+ y: 1,
622
+ width: 1,
623
+ height: 1
624
+ }]
625
+ );
626
+ }
627
+
628
+ #[test]
629
+ fn pinned_pages_fail_closed_and_recycle_only_after_release() {
630
+ let mut atlas = AtlasSession::new(AtlasConfig {
631
+ page_width: 8,
632
+ page_height: 8,
633
+ soft_pages: 1,
634
+ hard_pages: 2,
635
+ })
636
+ .unwrap();
637
+ let first = atlas.put(glyph("alpha", 10)).unwrap();
638
+ let lease_a = atlas.acquire(&["alpha".to_string()]).unwrap();
639
+ atlas.put(glyph("beta", 20)).unwrap();
640
+ let lease_b = atlas.acquire(&["beta".to_string()]).unwrap();
641
+ assert!(matches!(
642
+ atlas.put(glyph("gamma", 30)),
643
+ Err(AtlasError::MemoryBudgetExceeded { .. })
644
+ ));
645
+ assert!(atlas.release(lease_a));
646
+ assert!(!atlas.release(lease_a));
647
+ let recycled = atlas.put(glyph("gamma", 30)).unwrap();
648
+ assert_eq!(recycled.page, first.page);
649
+ assert_ne!(recycled.page_epoch, first.page_epoch);
650
+ assert!(atlas.release(lease_b));
651
+ }
652
+ }