@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,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Immutable authored-layer command ownership plus mutable render state.
|
|
3
|
+
* Visibility and dynamic deltas update only the dense state table; command
|
|
4
|
+
* order and resource identity remain stable for the plan lifetime.
|
|
5
|
+
*/
|
|
6
|
+
export class SemanticCommandPlan {
|
|
7
|
+
layerTableRevision;
|
|
8
|
+
commandRevision = 1;
|
|
9
|
+
resourceRevision = 1;
|
|
10
|
+
commands;
|
|
11
|
+
operationLayers;
|
|
12
|
+
stateByLayer = new Map();
|
|
13
|
+
resources;
|
|
14
|
+
commandVisible;
|
|
15
|
+
commandTransforms;
|
|
16
|
+
constructor(input) {
|
|
17
|
+
this.layerTableRevision = input.layerTableRevision;
|
|
18
|
+
this.commands = [...input.semanticCommands];
|
|
19
|
+
const stateBySlot = input.commandStates ?? this.commands.map((command, slot) => ({
|
|
20
|
+
command_id: command.id,
|
|
21
|
+
slot,
|
|
22
|
+
render_mask: true,
|
|
23
|
+
transform: { dx: 0, dy: 0 },
|
|
24
|
+
}));
|
|
25
|
+
if (stateBySlot.length !== this.commands.length)
|
|
26
|
+
throw new Error("command state table length mismatch");
|
|
27
|
+
this.commandVisible = new Array(this.commands.length);
|
|
28
|
+
this.commandTransforms = new Array(this.commands.length);
|
|
29
|
+
for (const state of stateBySlot) {
|
|
30
|
+
if (!Number.isInteger(state.slot) || state.slot < 0 || state.slot >= this.commands.length || this.commands[state.slot].id !== state.command_id) {
|
|
31
|
+
throw new Error(`command state identity mismatch at ${state.slot}`);
|
|
32
|
+
}
|
|
33
|
+
this.commandVisible[state.slot] = state.render_mask;
|
|
34
|
+
this.commandTransforms[state.slot] = { ...state.transform };
|
|
35
|
+
}
|
|
36
|
+
const tableByLayer = new Map();
|
|
37
|
+
const matrixByLayer = new Map(input.layerSources.map((layer) => [layer.layer_id, layer.matrix]));
|
|
38
|
+
for (const entry of input.layerTable) {
|
|
39
|
+
if (tableByLayer.has(entry.layer_id))
|
|
40
|
+
throw new Error(`duplicate layer table id ${entry.layer_id}`);
|
|
41
|
+
if (!Number.isInteger(entry.slot) || entry.slot < 0)
|
|
42
|
+
throw new Error(`invalid layer slot ${entry.slot}`);
|
|
43
|
+
tableByLayer.set(entry.layer_id, entry.slot);
|
|
44
|
+
}
|
|
45
|
+
const coverage = new Uint8Array(this.commands.length);
|
|
46
|
+
this.operationLayers = new Array(this.commands.length);
|
|
47
|
+
for (const layerCommand of input.layerCommands) {
|
|
48
|
+
const slot = tableByLayer.get(layerCommand.layer_id);
|
|
49
|
+
if (slot == null)
|
|
50
|
+
throw new Error(`layer command references unknown layer ${layerCommand.layer_id}`);
|
|
51
|
+
if (this.stateByLayer.has(layerCommand.layer_id)) {
|
|
52
|
+
throw new Error(`duplicate layer command ${layerCommand.layer_id}`);
|
|
53
|
+
}
|
|
54
|
+
const baseMatrix = matrixByLayer.get(layerCommand.layer_id);
|
|
55
|
+
if (!baseMatrix)
|
|
56
|
+
throw new Error(`missing authored layer matrix ${layerCommand.layer_id}`);
|
|
57
|
+
const start = layerCommand.command_start;
|
|
58
|
+
const end = start + layerCommand.command_count;
|
|
59
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end > this.commands.length) {
|
|
60
|
+
throw new Error(`invalid command span for layer ${layerCommand.layer_id}`);
|
|
61
|
+
}
|
|
62
|
+
for (let index = start; index < end; index += 1) {
|
|
63
|
+
const command = this.commands[index];
|
|
64
|
+
if (coverage[index] !== 0 || command.layer_id !== layerCommand.layer_id) {
|
|
65
|
+
throw new Error(`command span ownership mismatch at ${index}`);
|
|
66
|
+
}
|
|
67
|
+
coverage[index] = 1;
|
|
68
|
+
this.operationLayers[index] = { layerId: layerCommand.layer_id, slot };
|
|
69
|
+
}
|
|
70
|
+
this.stateByLayer.set(layerCommand.layer_id, {
|
|
71
|
+
slot,
|
|
72
|
+
baseMatrix: [...baseMatrix],
|
|
73
|
+
visible: layerCommand.render_mask,
|
|
74
|
+
transform: { ...layerCommand.transform },
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const uncovered = coverage.findIndex((value) => value === 0);
|
|
78
|
+
if (uncovered >= 0)
|
|
79
|
+
throw new Error(`semantic command ${uncovered} is outside every authored layer span`);
|
|
80
|
+
this.resources = collectResourceRequests(this.commands);
|
|
81
|
+
}
|
|
82
|
+
operations() {
|
|
83
|
+
return this.commands.map((command, index) => {
|
|
84
|
+
const ownership = this.operationLayers[index];
|
|
85
|
+
const state = this.stateByLayer.get(ownership.layerId);
|
|
86
|
+
if (!state)
|
|
87
|
+
throw new Error(`missing runtime layer state ${ownership.layerId}`);
|
|
88
|
+
return {
|
|
89
|
+
command,
|
|
90
|
+
layerId: ownership.layerId,
|
|
91
|
+
layerSlot: ownership.slot,
|
|
92
|
+
baseMatrix: [...state.baseMatrix],
|
|
93
|
+
visible: state.visible,
|
|
94
|
+
transform: { ...state.transform },
|
|
95
|
+
commandSlot: index,
|
|
96
|
+
commandVisible: this.commandVisible[index],
|
|
97
|
+
commandTransform: { ...this.commandTransforms[index] },
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
resourceRequests() {
|
|
102
|
+
return this.resources.map((resource) => ({ ...resource }));
|
|
103
|
+
}
|
|
104
|
+
applyLayerPatches(patches) {
|
|
105
|
+
for (const patch of patches) {
|
|
106
|
+
const state = this.stateByLayer.get(patch.layer_id);
|
|
107
|
+
if (!state)
|
|
108
|
+
throw new Error(`unknown layer patch ${patch.layer_id}`);
|
|
109
|
+
if (patch.render_mask != null)
|
|
110
|
+
state.visible = patch.render_mask;
|
|
111
|
+
if (patch.transform != null)
|
|
112
|
+
state.transform = { ...patch.transform };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
applyCommandPatches(patches) {
|
|
116
|
+
for (const patch of patches) {
|
|
117
|
+
if (!Number.isInteger(patch.slot) || patch.slot < 0 || patch.slot >= this.commands.length)
|
|
118
|
+
throw new Error(`invalid command slot ${patch.slot}`);
|
|
119
|
+
if (patch.render_mask != null)
|
|
120
|
+
this.commandVisible[patch.slot] = patch.render_mask;
|
|
121
|
+
if (patch.transform != null)
|
|
122
|
+
this.commandTransforms[patch.slot] = { ...patch.transform };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
export function semanticCommandPlanFromCoreSnapshot(snapshot) {
|
|
127
|
+
return new SemanticCommandPlan({
|
|
128
|
+
layerTableRevision: snapshot.revisions.layer_table,
|
|
129
|
+
layerTable: snapshot.layer_table,
|
|
130
|
+
layerSources: snapshot.layer_sources.map((layer) => ({ layer_id: layer.id, matrix: layer.matrix })),
|
|
131
|
+
layerCommands: snapshot.commands,
|
|
132
|
+
semanticCommands: snapshot.semantic_commands,
|
|
133
|
+
commandStates: snapshot.command_states,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
/** Adapts the native/server debug dump to the exact same immutable browser
|
|
137
|
+
* command plan used by a live WASM Scene. This is intentionally strict so a
|
|
138
|
+
* dump cannot silently reorder authored layers or command ownership. */
|
|
139
|
+
export function semanticCommandPlanFromCoreDump(dump) {
|
|
140
|
+
const layerById = new Map(dump.layers.map((layer) => [layer.layer_id, layer]));
|
|
141
|
+
const semanticCommands = [];
|
|
142
|
+
const layerCommands = [];
|
|
143
|
+
const layerSources = [];
|
|
144
|
+
for (const entry of [...dump.layer_table].sort((left, right) => left.slot - right.slot)) {
|
|
145
|
+
const layer = layerById.get(entry.layer_id);
|
|
146
|
+
if (!layer)
|
|
147
|
+
throw new Error(`core dump missing layer ${entry.layer_id}`);
|
|
148
|
+
const commandStart = semanticCommands.length;
|
|
149
|
+
semanticCommands.push(...layer.commands);
|
|
150
|
+
layerSources.push({ layer_id: layer.layer_id, matrix: layer.matrix });
|
|
151
|
+
layerCommands.push({
|
|
152
|
+
layer_id: layer.layer_id,
|
|
153
|
+
render_mask: layer.render_mask,
|
|
154
|
+
transform: { dx: 0, dy: 0 },
|
|
155
|
+
command_start: commandStart,
|
|
156
|
+
command_count: layer.commands.length,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
if (layerById.size !== dump.layer_table.length)
|
|
160
|
+
throw new Error("core dump contains layers outside the layer table");
|
|
161
|
+
return new SemanticCommandPlan({
|
|
162
|
+
layerTableRevision: dump.revisions.layer_table,
|
|
163
|
+
layerTable: dump.layer_table,
|
|
164
|
+
layerSources,
|
|
165
|
+
layerCommands,
|
|
166
|
+
semanticCommands,
|
|
167
|
+
commandStates: dump.command_states,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
function collectResourceRequests(commands) {
|
|
171
|
+
const deduplicated = new Map();
|
|
172
|
+
for (const command of commands) {
|
|
173
|
+
const resource = commandResource(command.payload);
|
|
174
|
+
if (!resource)
|
|
175
|
+
continue;
|
|
176
|
+
deduplicated.set(`${resource.namespace}\0${resource.key}`, resource);
|
|
177
|
+
if (command.payload.kind === "image") {
|
|
178
|
+
const mask = asResourceKey(command.payload.alpha_mask);
|
|
179
|
+
if (mask)
|
|
180
|
+
deduplicated.set(`${mask.namespace}\0${mask.key}`, mask);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return [...deduplicated.values()].map((resource) => ({ ...resource }));
|
|
184
|
+
}
|
|
185
|
+
function commandResource(payload) {
|
|
186
|
+
if (payload.kind === "image")
|
|
187
|
+
return asResourceKey(payload.resource);
|
|
188
|
+
if (payload.kind !== "shape")
|
|
189
|
+
return null;
|
|
190
|
+
const primitive = payload.primitive;
|
|
191
|
+
if (!primitive || typeof primitive !== "object" || Array.isArray(primitive))
|
|
192
|
+
return null;
|
|
193
|
+
const assetMask = primitive.asset_mask;
|
|
194
|
+
if (!assetMask || typeof assetMask !== "object" || Array.isArray(assetMask))
|
|
195
|
+
return null;
|
|
196
|
+
return asResourceKey(assetMask.resource);
|
|
197
|
+
}
|
|
198
|
+
function asResourceKey(value) {
|
|
199
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
200
|
+
return null;
|
|
201
|
+
const { namespace, key } = value;
|
|
202
|
+
return typeof namespace === "string" && typeof key === "string" ? { namespace, key } : null;
|
|
203
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { GlyphInstance } from "../types/glyph.js";
|
|
2
|
+
import type { SemanticDrawOperation } from "./semanticCommandPlanner.js";
|
|
3
|
+
export declare function compileSemanticTextGlyphBatches(instances: GlyphInstance[], textOperations: SemanticDrawOperation[]): Map<string, Float32Array>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { buildSdfGlyphInstanceVertices } from "./webglSdfGlyphPipeline.js";
|
|
2
|
+
export function compileSemanticTextGlyphBatches(instances, textOperations) {
|
|
3
|
+
const result = new Map();
|
|
4
|
+
for (const operation of textOperations) {
|
|
5
|
+
if (operation.command.payload.kind !== "text")
|
|
6
|
+
throw new Error(`non-text glyph bridge operation ${operation.command.id}`);
|
|
7
|
+
const commandInstances = instances.filter((instance) => instance.layerId === operation.command.id);
|
|
8
|
+
const clips = operation.command.clip == null
|
|
9
|
+
? new Map()
|
|
10
|
+
: new Map([[operation.command.id, transformClip(operation.command.clip, operation.baseMatrix, operation.command.id)]]);
|
|
11
|
+
result.set(operation.command.id, buildSdfGlyphInstanceVertices(commandInstances, new Map([[operation.command.id, operation.layerSlot]]), clips, new Map([[operation.command.id, operation.commandSlot]])));
|
|
12
|
+
}
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
function transformClip(value, matrix, commandId) {
|
|
16
|
+
if (!Array.isArray(value) || value.length !== 4 || value.some((point) => !Array.isArray(point) || point.length !== 2))
|
|
17
|
+
throw new Error(`invalid text clip ${commandId}`);
|
|
18
|
+
return value.map((point) => [
|
|
19
|
+
matrix[0] * Number(point[0]) + matrix[2] * Number(point[1]) + matrix[4],
|
|
20
|
+
matrix[1] * Number(point[0]) + matrix[3] * Number(point[1]) + matrix[5],
|
|
21
|
+
]);
|
|
22
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { SdfAtlas } from "../fontSdfAtlas.js";
|
|
2
|
+
import type { WasmLayoutBatch } from "../types/layout.js";
|
|
3
|
+
import type { SemanticCommandPlan, SemanticCommandStatePatch, SemanticLayerPatch } from "./semanticCommandPlanner.js";
|
|
4
|
+
import { type SemanticGpuMetrics } from "./webglSemanticCommandExecutor.js";
|
|
5
|
+
import { type NumericTextRegion } from "../interaction/numericTextRegions.js";
|
|
6
|
+
import type { BrowserImageSource } from "./browserSemanticResources.js";
|
|
7
|
+
import type { GlyphInstance } from "../types/glyph.js";
|
|
8
|
+
import type { SemanticDrawOperation } from "./semanticCommandPlanner.js";
|
|
9
|
+
export type AuthoredTextHitGeometry = {
|
|
10
|
+
bounds: {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
};
|
|
16
|
+
quad: [[number, number], [number, number], [number, number], [number, number]];
|
|
17
|
+
};
|
|
18
|
+
export declare class SemanticWebglSceneRenderer {
|
|
19
|
+
private executor;
|
|
20
|
+
private plan;
|
|
21
|
+
private numericRegions;
|
|
22
|
+
private textHitGeometry;
|
|
23
|
+
private contextLost;
|
|
24
|
+
private retainedScene;
|
|
25
|
+
constructor(gl: WebGL2RenderingContext);
|
|
26
|
+
setScene(input: {
|
|
27
|
+
plan: SemanticCommandPlan;
|
|
28
|
+
atlas: SdfAtlas | null;
|
|
29
|
+
layout: WasmLayoutBatch;
|
|
30
|
+
imageSources: Map<string, BrowserImageSource>;
|
|
31
|
+
}): Promise<{
|
|
32
|
+
textCommands: number;
|
|
33
|
+
glyphInstances: number;
|
|
34
|
+
atlasUploadBytes: number;
|
|
35
|
+
atlasUploadRects: number;
|
|
36
|
+
}>;
|
|
37
|
+
applyLayerPatches(patches: SemanticLayerPatch[]): {
|
|
38
|
+
stateUploadBytes: number;
|
|
39
|
+
maskUploadBytes: number;
|
|
40
|
+
};
|
|
41
|
+
applyCommandPatches(patches: SemanticCommandStatePatch[]): {
|
|
42
|
+
commandMaskUploadBytes: number;
|
|
43
|
+
commandStateUploadBytes: number;
|
|
44
|
+
};
|
|
45
|
+
applyCoreDelta(delta: {
|
|
46
|
+
patches: SemanticLayerPatch[];
|
|
47
|
+
command_patches: SemanticCommandStatePatch[];
|
|
48
|
+
}): {
|
|
49
|
+
commandMaskUploadBytes: number;
|
|
50
|
+
commandStateUploadBytes: number;
|
|
51
|
+
stateUploadBytes: number;
|
|
52
|
+
maskUploadBytes: number;
|
|
53
|
+
};
|
|
54
|
+
setLayerPreviewTransform(layerId: string, matrix: [number, number, number, number, number, number] | null): {
|
|
55
|
+
previewUploadBytes: number;
|
|
56
|
+
};
|
|
57
|
+
draw(): SemanticGpuMetrics;
|
|
58
|
+
notifyContextLost(): void;
|
|
59
|
+
restoreContext(gl: WebGL2RenderingContext): Promise<{
|
|
60
|
+
atlasUploadBytes: number;
|
|
61
|
+
atlasUploadRects: number;
|
|
62
|
+
textureUploads: number;
|
|
63
|
+
textureBytes: number;
|
|
64
|
+
}>;
|
|
65
|
+
interactionRegions(): NumericTextRegion[];
|
|
66
|
+
authoredTextHitGeometry(): Map<string, AuthoredTextHitGeometry>;
|
|
67
|
+
destroy(): void;
|
|
68
|
+
private assertContextReady;
|
|
69
|
+
}
|
|
70
|
+
export declare function buildAuthoredTextHitGeometry(operations: SemanticDrawOperation[], instances: GlyphInstance[]): Map<string, AuthoredTextHitGeometry>;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { compileSemanticDrawBatches, semanticTextBatchKey } from "./semanticCommandGeometry.js";
|
|
2
|
+
import { compileSemanticTextGlyphBatches } from "./semanticTextGlyphBridge.js";
|
|
3
|
+
import { WebglSemanticCommandExecutor } from "./webglSemanticCommandExecutor.js";
|
|
4
|
+
import { applyNumericRegionRuntimeState, buildNumericTextRegions } from "../interaction/numericTextRegions.js";
|
|
5
|
+
import { placeGeneralTextInstances } from "./generalTextRenderPlacement.js";
|
|
6
|
+
export class SemanticWebglSceneRenderer {
|
|
7
|
+
executor;
|
|
8
|
+
plan = null;
|
|
9
|
+
numericRegions = [];
|
|
10
|
+
textHitGeometry = new Map();
|
|
11
|
+
contextLost = false;
|
|
12
|
+
retainedScene = null;
|
|
13
|
+
constructor(gl) {
|
|
14
|
+
this.executor = new WebglSemanticCommandExecutor(gl);
|
|
15
|
+
}
|
|
16
|
+
async setScene(input) {
|
|
17
|
+
const operations = input.plan.operations();
|
|
18
|
+
this.plan = input.plan;
|
|
19
|
+
const batches = compileSemanticDrawBatches(operations);
|
|
20
|
+
this.executor.setScene(input.plan, batches, input.imageSources);
|
|
21
|
+
const atlasUpload = input.atlas
|
|
22
|
+
? await this.executor.setSdfAtlas(input.atlas)
|
|
23
|
+
: { bytes: 0, rects: 0 };
|
|
24
|
+
const textOperations = operations.filter((operation) => operation.command.payload.kind === "text");
|
|
25
|
+
const placedInstances = placeGeneralTextInstances(input.layout.instances, textOperations);
|
|
26
|
+
this.textHitGeometry = buildAuthoredTextHitGeometry(textOperations, placedInstances);
|
|
27
|
+
this.numericRegions = textOperations.flatMap((operation) => buildNumericTextRegions(operation.command, placedInstances, operation.visible && operation.commandVisible));
|
|
28
|
+
const glyphsByCommand = compileSemanticTextGlyphBatches(placedInstances, textOperations);
|
|
29
|
+
const textGlyphBatches = new Map();
|
|
30
|
+
for (const batch of batches.filter((batch) => batch.kind === "text")) {
|
|
31
|
+
const vertices = concatenateFloat32(batch.commandIds.map((commandId) => glyphsByCommand.get(commandId) ?? new Float32Array()));
|
|
32
|
+
const key = semanticTextBatchKey(batch.commandIds);
|
|
33
|
+
textGlyphBatches.set(key, vertices);
|
|
34
|
+
this.executor.setTextGlyphBatch(key, vertices);
|
|
35
|
+
}
|
|
36
|
+
this.retainedScene = {
|
|
37
|
+
plan: input.plan,
|
|
38
|
+
atlas: input.atlas,
|
|
39
|
+
batches,
|
|
40
|
+
imageSources: input.imageSources,
|
|
41
|
+
textGlyphBatches,
|
|
42
|
+
};
|
|
43
|
+
this.contextLost = false;
|
|
44
|
+
return {
|
|
45
|
+
textCommands: textOperations.length,
|
|
46
|
+
glyphInstances: input.layout.instances.length,
|
|
47
|
+
atlasUploadBytes: atlasUpload.bytes,
|
|
48
|
+
atlasUploadRects: atlasUpload.rects,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
applyLayerPatches(patches) {
|
|
52
|
+
this.assertContextReady();
|
|
53
|
+
return this.executor.applyLayerPatches(patches);
|
|
54
|
+
}
|
|
55
|
+
applyCommandPatches(patches) {
|
|
56
|
+
this.assertContextReady();
|
|
57
|
+
return this.executor.applyCommandPatches(patches);
|
|
58
|
+
}
|
|
59
|
+
applyCoreDelta(delta) {
|
|
60
|
+
this.assertContextReady();
|
|
61
|
+
const layer = this.executor.applyLayerPatches(delta.patches);
|
|
62
|
+
const command = this.executor.applyCommandPatches(delta.command_patches);
|
|
63
|
+
return { ...layer, ...command };
|
|
64
|
+
}
|
|
65
|
+
setLayerPreviewTransform(layerId, matrix) {
|
|
66
|
+
this.assertContextReady();
|
|
67
|
+
return this.executor.setLayerPreviewTransform(layerId, matrix);
|
|
68
|
+
}
|
|
69
|
+
draw() {
|
|
70
|
+
this.assertContextReady();
|
|
71
|
+
return this.executor.draw();
|
|
72
|
+
}
|
|
73
|
+
notifyContextLost() {
|
|
74
|
+
this.contextLost = true;
|
|
75
|
+
}
|
|
76
|
+
async restoreContext(gl) {
|
|
77
|
+
const retained = this.retainedScene;
|
|
78
|
+
if (!retained)
|
|
79
|
+
throw new Error("semantic GPU scene is not initialized");
|
|
80
|
+
const previous = this.executor;
|
|
81
|
+
const restored = new WebglSemanticCommandExecutor(gl);
|
|
82
|
+
try {
|
|
83
|
+
restored.setScene(retained.plan, retained.batches, retained.imageSources);
|
|
84
|
+
const atlasUpload = retained.atlas
|
|
85
|
+
? await restored.setSdfAtlas(retained.atlas)
|
|
86
|
+
: { bytes: 0, rects: 0 };
|
|
87
|
+
for (const [key, vertices] of retained.textGlyphBatches)
|
|
88
|
+
restored.setTextGlyphBatch(key, vertices);
|
|
89
|
+
this.executor = restored;
|
|
90
|
+
this.contextLost = false;
|
|
91
|
+
previous.destroy();
|
|
92
|
+
return {
|
|
93
|
+
atlasUploadBytes: atlasUpload.bytes,
|
|
94
|
+
atlasUploadRects: atlasUpload.rects,
|
|
95
|
+
textureUploads: retained.imageSources.size,
|
|
96
|
+
textureBytes: imageSourceBytes(retained.imageSources.values()),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
restored.destroy();
|
|
101
|
+
this.contextLost = true;
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
interactionRegions() {
|
|
106
|
+
const operations = new Map((this.plan?.operations() ?? []).map((operation) => [operation.command.id, operation]));
|
|
107
|
+
return this.numericRegions.map((region) => {
|
|
108
|
+
const operation = operations.get(region.id.split(":numeric:", 1)[0]);
|
|
109
|
+
if (!operation)
|
|
110
|
+
return region;
|
|
111
|
+
return {
|
|
112
|
+
...applyNumericRegionRuntimeState(region, {
|
|
113
|
+
layerTransform: operation.transform,
|
|
114
|
+
commandTransform: operation.commandTransform,
|
|
115
|
+
}),
|
|
116
|
+
render_mask: operation.visible && operation.commandVisible,
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
authoredTextHitGeometry() {
|
|
121
|
+
const operations = this.plan?.operations() ?? [];
|
|
122
|
+
const runtimeByLayer = new Map(operations.map((operation) => [operation.layerId, operation]));
|
|
123
|
+
return new Map([...this.textHitGeometry].map(([layerId, geometry]) => {
|
|
124
|
+
const operation = runtimeByLayer.get(layerId);
|
|
125
|
+
const dx = (operation?.transform.dx ?? 0) + (operation?.commandTransform.dx ?? 0);
|
|
126
|
+
const dy = (operation?.transform.dy ?? 0) + (operation?.commandTransform.dy ?? 0);
|
|
127
|
+
const quad = geometry.quad.map(([x, y]) => [x + dx, y + dy]);
|
|
128
|
+
return [layerId, {
|
|
129
|
+
bounds: { ...geometry.bounds, x: geometry.bounds.x + dx, y: geometry.bounds.y + dy },
|
|
130
|
+
quad,
|
|
131
|
+
}];
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
destroy() {
|
|
135
|
+
this.executor.destroy();
|
|
136
|
+
this.plan = null;
|
|
137
|
+
this.numericRegions = [];
|
|
138
|
+
this.textHitGeometry.clear();
|
|
139
|
+
this.retainedScene = null;
|
|
140
|
+
this.contextLost = false;
|
|
141
|
+
}
|
|
142
|
+
assertContextReady() {
|
|
143
|
+
if (this.contextLost)
|
|
144
|
+
throw new Error("WebGL context is lost");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
export function buildAuthoredTextHitGeometry(operations, instances) {
|
|
148
|
+
const output = new Map();
|
|
149
|
+
for (const operation of operations) {
|
|
150
|
+
const quads = instances
|
|
151
|
+
.filter((instance) => instance.layerId === operation.command.id)
|
|
152
|
+
.map((instance) => instance.deviceCharQuad[1])
|
|
153
|
+
.filter((quad) => quad.length >= 4);
|
|
154
|
+
const points = quads.flat();
|
|
155
|
+
if (points.length === 0)
|
|
156
|
+
continue;
|
|
157
|
+
const basis = quads.find((quad) => (Math.hypot(quad[1][0] - quad[0][0], quad[1][1] - quad[0][1]) > 1e-4
|
|
158
|
+
&& Math.hypot(quad[3][0] - quad[0][0], quad[3][1] - quad[0][1]) > 1e-4));
|
|
159
|
+
if (!basis)
|
|
160
|
+
continue;
|
|
161
|
+
const ux0 = basis[1][0] - basis[0][0];
|
|
162
|
+
const uy0 = basis[1][1] - basis[0][1];
|
|
163
|
+
const vx0 = basis[3][0] - basis[0][0];
|
|
164
|
+
const vy0 = basis[3][1] - basis[0][1];
|
|
165
|
+
const ul = Math.hypot(ux0, uy0);
|
|
166
|
+
const vl = Math.hypot(vx0, vy0);
|
|
167
|
+
const ux = ux0 / ul;
|
|
168
|
+
const uy = uy0 / ul;
|
|
169
|
+
const vx = vx0 / vl;
|
|
170
|
+
const vy = vy0 / vl;
|
|
171
|
+
const uValues = points.map(([x, y]) => x * ux + y * uy);
|
|
172
|
+
const vValues = points.map(([x, y]) => x * vx + y * vy);
|
|
173
|
+
const minU = Math.min(...uValues);
|
|
174
|
+
const maxU = Math.max(...uValues);
|
|
175
|
+
const minV = Math.min(...vValues);
|
|
176
|
+
const maxV = Math.max(...vValues);
|
|
177
|
+
const point = (u, v) => [ux * u + vx * v, uy * u + vy * v];
|
|
178
|
+
const quad = [
|
|
179
|
+
point(minU, minV),
|
|
180
|
+
point(maxU, minV),
|
|
181
|
+
point(maxU, maxV),
|
|
182
|
+
point(minU, maxV),
|
|
183
|
+
];
|
|
184
|
+
const xs = quad.map(([x]) => x);
|
|
185
|
+
const ys = quad.map(([, y]) => y);
|
|
186
|
+
const minX = Math.min(...xs);
|
|
187
|
+
const maxX = Math.max(...xs);
|
|
188
|
+
const minY = Math.min(...ys);
|
|
189
|
+
const maxY = Math.max(...ys);
|
|
190
|
+
output.set(operation.layerId, {
|
|
191
|
+
bounds: { x: minX, y: minY, width: maxX - minX, height: maxY - minY },
|
|
192
|
+
quad,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return output;
|
|
196
|
+
}
|
|
197
|
+
function concatenateFloat32(values) {
|
|
198
|
+
const output = new Float32Array(values.reduce((sum, value) => sum + value.length, 0));
|
|
199
|
+
let offset = 0;
|
|
200
|
+
for (const value of values) {
|
|
201
|
+
output.set(value, offset);
|
|
202
|
+
offset += value.length;
|
|
203
|
+
}
|
|
204
|
+
return output;
|
|
205
|
+
}
|
|
206
|
+
function imageSourceBytes(values) {
|
|
207
|
+
let bytes = 0;
|
|
208
|
+
for (const source of values)
|
|
209
|
+
bytes += Math.max(0, source.width * source.height * 4);
|
|
210
|
+
return bytes;
|
|
211
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function contiguousSlotRanges(slots) {
|
|
2
|
+
const sorted = [...new Set(slots)].sort((left, right) => left - right);
|
|
3
|
+
const ranges = [];
|
|
4
|
+
for (const slot of sorted) {
|
|
5
|
+
const last = ranges.at(-1);
|
|
6
|
+
if (last && slot === last.end) {
|
|
7
|
+
last.end += 1;
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
ranges.push({ start: slot, end: slot + 1 });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return ranges;
|
|
14
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { SdfAtlas } from "../fontSdfAtlas.js";
|
|
2
|
+
export declare class WebglSdfAtlasTexture {
|
|
3
|
+
private readonly gl;
|
|
4
|
+
private readonly maxPages;
|
|
5
|
+
private textureValue;
|
|
6
|
+
private contractId;
|
|
7
|
+
private revisions;
|
|
8
|
+
private allocated;
|
|
9
|
+
constructor(gl: WebGL2RenderingContext, maxPages?: number);
|
|
10
|
+
texture(): WebGLTexture;
|
|
11
|
+
setAtlas(atlas: SdfAtlas): void;
|
|
12
|
+
uploadUpdates(atlas: SdfAtlas): Promise<{
|
|
13
|
+
bytes: number;
|
|
14
|
+
rects: number;
|
|
15
|
+
}>;
|
|
16
|
+
destroy(): void;
|
|
17
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export class WebglSdfAtlasTexture {
|
|
2
|
+
gl;
|
|
3
|
+
maxPages;
|
|
4
|
+
textureValue;
|
|
5
|
+
contractId = null;
|
|
6
|
+
revisions = new Map();
|
|
7
|
+
allocated = false;
|
|
8
|
+
constructor(gl, maxPages = 6) {
|
|
9
|
+
this.gl = gl;
|
|
10
|
+
this.maxPages = maxPages;
|
|
11
|
+
const texture = gl.createTexture();
|
|
12
|
+
if (!texture)
|
|
13
|
+
throw new Error("SDF atlas texture allocation failed");
|
|
14
|
+
this.textureValue = texture;
|
|
15
|
+
}
|
|
16
|
+
texture() {
|
|
17
|
+
return this.textureValue;
|
|
18
|
+
}
|
|
19
|
+
setAtlas(atlas) {
|
|
20
|
+
if (this.contractId === atlas.contractId)
|
|
21
|
+
return;
|
|
22
|
+
this.contractId = atlas.contractId;
|
|
23
|
+
this.revisions.clear();
|
|
24
|
+
this.allocated = false;
|
|
25
|
+
this.gl.deleteTexture(this.textureValue);
|
|
26
|
+
const texture = this.gl.createTexture();
|
|
27
|
+
if (!texture)
|
|
28
|
+
throw new Error("SDF atlas texture reallocation failed");
|
|
29
|
+
this.textureValue = texture;
|
|
30
|
+
}
|
|
31
|
+
async uploadUpdates(atlas) {
|
|
32
|
+
this.setAtlas(atlas);
|
|
33
|
+
const updates = await atlas.pageUpdates(this.revisions);
|
|
34
|
+
if (updates.length === 0)
|
|
35
|
+
return { bytes: 0, rects: 0 };
|
|
36
|
+
const gl = this.gl;
|
|
37
|
+
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 0);
|
|
38
|
+
gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.textureValue);
|
|
39
|
+
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
40
|
+
if (!this.allocated) {
|
|
41
|
+
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
42
|
+
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
43
|
+
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
44
|
+
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
45
|
+
gl.texStorage3D(gl.TEXTURE_2D_ARRAY, 1, gl.R8, atlas.width, atlas.height, this.maxPages);
|
|
46
|
+
this.allocated = true;
|
|
47
|
+
}
|
|
48
|
+
let bytes = 0;
|
|
49
|
+
let rects = 0;
|
|
50
|
+
for (const update of updates) {
|
|
51
|
+
if (update.page >= this.maxPages)
|
|
52
|
+
throw new Error(`atlas page ${update.page} exceeds GPU hard budget`);
|
|
53
|
+
if (update.fullUpload) {
|
|
54
|
+
gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, 0, 0, 0, update.page, atlas.width, atlas.height, 1, gl.RED, gl.UNSIGNED_BYTE, update.pixels);
|
|
55
|
+
bytes += update.pixels.byteLength;
|
|
56
|
+
rects += 1;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
for (const rect of update.dirtyRects) {
|
|
60
|
+
gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, 0, rect.x, rect.y, update.page, rect.width, rect.height, 1, gl.RED, gl.UNSIGNED_BYTE, rect.pixels);
|
|
61
|
+
bytes += rect.pixels.byteLength;
|
|
62
|
+
rects += 1;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
this.revisions.set(update.page, update.revision);
|
|
66
|
+
}
|
|
67
|
+
return { bytes, rects };
|
|
68
|
+
}
|
|
69
|
+
destroy() {
|
|
70
|
+
this.gl.deleteTexture(this.textureValue);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { GlyphInstance } from "../types/glyph.js";
|
|
2
|
+
export declare class WebglSdfGlyphPipeline {
|
|
3
|
+
private readonly gl;
|
|
4
|
+
private readonly program;
|
|
5
|
+
private readonly batches;
|
|
6
|
+
private atlasTexture;
|
|
7
|
+
private ownsAtlas;
|
|
8
|
+
private geometryBuilds;
|
|
9
|
+
constructor(gl: WebGL2RenderingContext);
|
|
10
|
+
setAtlas(texture: WebGLTexture): void;
|
|
11
|
+
upload(key: string, vertices: Float32Array): void;
|
|
12
|
+
draw(key: string, stateTexture: WebGLTexture, maskTexture: WebGLTexture, stateWidth: number, commandMaskTexture?: WebGLTexture, commandStateTexture?: WebGLTexture, commandWidth?: number, previewTransformTexture?: WebGLTexture): {
|
|
13
|
+
drawCalls: number;
|
|
14
|
+
instances: number;
|
|
15
|
+
bytes: number;
|
|
16
|
+
};
|
|
17
|
+
stats(): {
|
|
18
|
+
geometryBuilds: number;
|
|
19
|
+
batches: number;
|
|
20
|
+
bytes: number;
|
|
21
|
+
};
|
|
22
|
+
clearBatches(): void;
|
|
23
|
+
destroy(): void;
|
|
24
|
+
private deleteBatch;
|
|
25
|
+
}
|
|
26
|
+
export declare function buildSdfGlyphInstanceVertices(instances: GlyphInstance[], layerSlots: ReadonlyMap<string, number>, clips?: ReadonlyMap<string, [[number, number], [number, number], [number, number], [number, number]]>, commandSlots?: ReadonlyMap<string, number>): Float32Array;
|