@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
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
|
+
}
|