@solidrt/2d 0.0.70 → 0.0.71
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/AGENTS.md +14 -1
- package/package.json +3 -3
- package/src/layer.ts +40 -32
- package/src/records.ts +74 -83
- package/src/text.ts +64 -11
package/AGENTS.md
CHANGED
|
@@ -267,7 +267,20 @@ mark their own record, so a `setSprite` here publishes one record, not
|
|
|
267
267
|
the prefix. The POPULATION verbs differ by kind and stay so: a record
|
|
268
268
|
mesh's records are opaque data (`setRecords` copies in and grows), a
|
|
269
269
|
record layer's are sprites (`addSprite`/`destroySprite`), so the copy-in
|
|
270
|
-
here is `records(layer).set(src)` then `updateRecords(layer)`.
|
|
270
|
+
here is `records(layer).set(src)` then `updateRecords(layer)`. The
|
|
271
|
+
publish itself is the core's record stream (`createRecordStream` in
|
|
272
|
+
`@solidrt/core/gpu`: the GPU buffer, the byte mirror, the dirty range,
|
|
273
|
+
the range-or-whole publish, growth into a replacement), the one model
|
|
274
|
+
the 3d instance streams run on too; this file only decides what to
|
|
275
|
+
mark and when to flush. The node layer speaks the same two verbs over
|
|
276
|
+
its STYLE records: `records(spriteLayer)` is the style mirror
|
|
277
|
+
(`STYLE_FLOATS` floats per slot: `[u0, v0, u1, v1, tint rgba,
|
|
278
|
+
renderOrder, minScreenPx, maxScreenPx, atlas, outline rgb,
|
|
279
|
+
outlineWidth]`, slot-indexed, poses stay the core's) and
|
|
280
|
+
`updateRecords(spriteLayer, { first?, count? })` publishes the slot
|
|
281
|
+
range, so a bulk restyle (a palette cycle over thousands of sprites) is
|
|
282
|
+
one loop and one range write instead of a setSprite each - the 3d
|
|
283
|
+
`records(instancedMesh)` pairing, one dimension down. It is the
|
|
271
284
|
escape hatch for motion only JS can compute at scale (measured 30k
|
|
272
285
|
sprites: 12.9ms raw records vs 30.8ms via setSprite; both figures are
|
|
273
286
|
the WRITE path only - whatever computes the motion is excluded and is
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/2d",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.71",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"funding": "https://github.com/sponsors/wellawaretech",
|
|
6
6
|
"author": "Antoine van Wel",
|
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
"AGENTS.md"
|
|
16
16
|
],
|
|
17
17
|
"devDependencies": {
|
|
18
|
-
"@solidrt/test": "0.0.
|
|
18
|
+
"@solidrt/test": "0.0.71"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
21
|
"@solidjs/signals": "2.0.0-rc.13",
|
|
22
|
-
"@solidrt/core": "0.0.
|
|
22
|
+
"@solidrt/core": "0.0.71"
|
|
23
23
|
}
|
|
24
24
|
}
|
package/src/layer.ts
CHANGED
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
// (dispatch.ts) undoes it with unprojectCamera.
|
|
36
36
|
import { getOwner, onBeforeRender, onCleanup, runWithOwner } from "@solidrt/core"
|
|
37
37
|
import type { PointerEvent as ElementPointerEvent, WheelEvent as ElementWheelEvent } from "@solidrt/core"
|
|
38
|
-
import {
|
|
39
|
-
import type { BlendMode, BufferId, TextureId } from "@solidrt/core/gpu"
|
|
38
|
+
import { checkScreenSize, createBuffer, createRecordStream, destroyBuffer, screenSizeScale } from "@solidrt/core/gpu"
|
|
39
|
+
import type { BlendMode, BufferId, RecordStream, TextureId } from "@solidrt/core/gpu"
|
|
40
40
|
import * as spatial from "flux:spatial"
|
|
41
41
|
import type {
|
|
42
42
|
Impact as CoreImpact,
|
|
@@ -591,6 +591,13 @@ export type LayerBase = {
|
|
|
591
591
|
_read(sprite: Sprite): Required<SpriteOptions>
|
|
592
592
|
_destroy(sprite: SpriteState): void
|
|
593
593
|
_schedule(): void
|
|
594
|
+
/** The layer's JS-owned record stream, what records()/updateRecords
|
|
595
|
+
* read and mark: a records layer's whole records, a node layer's style
|
|
596
|
+
* records (its poses are the core's). Internal. */
|
|
597
|
+
_stream: RecordStream
|
|
598
|
+
/** The stream's mirror as floats, re-pointed by growth; records() hands
|
|
599
|
+
* it out. Internal. */
|
|
600
|
+
_records: Float32Array
|
|
594
601
|
}
|
|
595
602
|
|
|
596
603
|
/** A query volume for overlap/sweep/moveAndSlide, in layer pixels: a
|
|
@@ -827,7 +834,12 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
827
834
|
let tint = opts?.tint ?? [1, 1, 1, 1]
|
|
828
835
|
checkTint("createSpriteLayer", tint)
|
|
829
836
|
let pose: BufferId = createBuffer(capacity * POSE_FLOATS * 4, { label: `${label}-pose`, autoFree: false })
|
|
830
|
-
|
|
837
|
+
// The style records: JS-written through the sprite verbs (writeStyle),
|
|
838
|
+
// records()/updateRecords for a bulk restyle, published by the flush as
|
|
839
|
+
// the dirty slot range, or the used prefix whole under an order (the
|
|
840
|
+
// core gathers both buffers under the one permutation).
|
|
841
|
+
let style = createRecordStream(STYLE_FLOATS * 4, capacity, { label: `${label}-style`, autoFree: false })
|
|
842
|
+
let ordered = opts?.orderBy !== undefined
|
|
831
843
|
let gpu = createSpritePipeline(label, VERTEX_SPLIT, INSTANCE_LAYOUTS_SPLIT, opts?.blend ?? "alpha", atlases)
|
|
832
844
|
// The sprites with a screen-size clamp (either bound on), and the
|
|
833
845
|
// furthest any of them reaches from its center at its floor (floorReach,
|
|
@@ -853,7 +865,6 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
853
865
|
|
|
854
866
|
let disposed = false
|
|
855
867
|
let scheduled = false
|
|
856
|
-
let styleDirty = false
|
|
857
868
|
let published = 0
|
|
858
869
|
// The publish pass of every frame runs the pending flush ahead of the
|
|
859
870
|
// paint, so a write made anywhere in the frame's JS is in that frame's
|
|
@@ -878,7 +889,7 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
878
889
|
// slot stays taken until the core says it is gone; dispose frees them.
|
|
879
890
|
let leaving = new Set<NodeId>()
|
|
880
891
|
let gpuCapacity = capacity
|
|
881
|
-
let styleData = new Float32Array(
|
|
892
|
+
let styleData = new Float32Array(style.bytes.buffer)
|
|
882
893
|
let byNode = new Map<NodeId, SpriteState>()
|
|
883
894
|
// The layer's root node (identity): parentless sprites and groups hang
|
|
884
895
|
// off it, so a query scoped to it sees exactly this layer.
|
|
@@ -909,12 +920,7 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
909
920
|
let flush = () => {
|
|
910
921
|
scheduled = false
|
|
911
922
|
if (disposed) return
|
|
912
|
-
if (
|
|
913
|
-
styleDirty = false
|
|
914
|
-
let out = beginBufferWrite(style)
|
|
915
|
-
out.set(styleData.subarray(0, highWater * STYLE_FLOATS))
|
|
916
|
-
endBufferWrite(style, highWater * STYLE_FLOATS * 4)
|
|
917
|
-
}
|
|
923
|
+
if (style.dirty) style.publish(ordered, highWater)
|
|
918
924
|
if (published !== highWater) {
|
|
919
925
|
views.setCount(highWater)
|
|
920
926
|
published = highWater
|
|
@@ -926,23 +932,19 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
926
932
|
|
|
927
933
|
// Grow both instance buffers to `next` slots: the pose sinks move in one
|
|
928
934
|
// retargetRecords call (the whole used range republishes at the next
|
|
929
|
-
// flush), the style
|
|
930
|
-
// The entries hold the old buffers alive until
|
|
931
|
-
// destroys are safe to issue right after.
|
|
935
|
+
// flush), the style stream grows into a replacement marked whole, which
|
|
936
|
+
// the next flush publishes. The entries hold the old buffers alive until
|
|
937
|
+
// the swaps land, so the destroys are safe to issue right after.
|
|
932
938
|
let grow = (next: number) => {
|
|
933
939
|
let newPose = createBuffer(next * POSE_FLOATS * 4, { label: `${label}-pose`, autoFree: false })
|
|
934
|
-
let newStyle = createBuffer(next * STYLE_FLOATS * 4, { label: `${label}-style`, autoFree: false })
|
|
935
940
|
spatial.retargetRecords(pose, newPose)
|
|
936
|
-
let
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
views.setBuffers([newPose, newStyle])
|
|
941
|
+
let oldStyle = style.grow(next)
|
|
942
|
+
styleData = new Float32Array(style.bytes.buffer)
|
|
943
|
+
views.setBuffers([newPose, style.buffer])
|
|
940
944
|
destroyBuffer(pose)
|
|
941
|
-
destroyBuffer(
|
|
945
|
+
destroyBuffer(oldStyle)
|
|
942
946
|
pose = newPose
|
|
943
|
-
style = newStyle
|
|
944
947
|
gpuCapacity = next
|
|
945
|
-
styleDirty = true
|
|
946
948
|
}
|
|
947
949
|
|
|
948
950
|
// A frame's atlas index, or a throw: run before ANY field of a write
|
|
@@ -956,6 +958,7 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
956
958
|
|
|
957
959
|
let writeStyle = (sprite: SpriteState, opts: SpriteOptions, atlas: number) => {
|
|
958
960
|
let at = sprite._slot * STYLE_FLOATS
|
|
961
|
+
let changed = false
|
|
959
962
|
let flipX = opts.flipX !== undefined && opts.flipX !== sprite._flipX
|
|
960
963
|
let flipY = opts.flipY !== undefined && opts.flipY !== sprite._flipY
|
|
961
964
|
if (flipX) sprite._flipX = !sprite._flipX
|
|
@@ -964,31 +967,32 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
964
967
|
let f = opts.frame
|
|
965
968
|
writeFrame(styleData, at, f.u0, f.v0, f.u1, f.v1, sprite._flipX, sprite._flipY)
|
|
966
969
|
styleData[at + STYLE_ATLAS_FIELD] = atlas
|
|
967
|
-
|
|
970
|
+
changed = true
|
|
968
971
|
} else if (flipX || flipY) {
|
|
969
972
|
// No new frame: toggle the changed axes on the stored UVs.
|
|
970
973
|
writeFrame(styleData, at, styleData[at]!, styleData[at + 1]!, styleData[at + 2]!, styleData[at + 3]!, flipX, flipY)
|
|
971
|
-
|
|
974
|
+
changed = true
|
|
972
975
|
}
|
|
973
976
|
if (opts.tint !== undefined) {
|
|
974
977
|
styleData[at + 4] = opts.tint[0]
|
|
975
978
|
styleData[at + 5] = opts.tint[1]
|
|
976
979
|
styleData[at + 6] = opts.tint[2]
|
|
977
980
|
styleData[at + 7] = opts.tint[3]
|
|
978
|
-
|
|
981
|
+
changed = true
|
|
979
982
|
}
|
|
980
983
|
if (opts.renderOrder !== undefined) {
|
|
981
984
|
styleData[at + STYLE_KEY_FIELD] = opts.renderOrder
|
|
982
|
-
|
|
985
|
+
changed = true
|
|
983
986
|
}
|
|
984
987
|
if (opts.minScreenPx !== undefined) {
|
|
985
988
|
styleData[at + STYLE_MIN_PX_FIELD] = opts.minScreenPx
|
|
986
|
-
|
|
989
|
+
changed = true
|
|
987
990
|
}
|
|
988
991
|
if (opts.maxScreenPx !== undefined) {
|
|
989
992
|
styleData[at + STYLE_MAX_PX_FIELD] = opts.maxScreenPx
|
|
990
|
-
|
|
993
|
+
changed = true
|
|
991
994
|
}
|
|
995
|
+
if (changed) style.mark(sprite._slot, sprite._slot + 1)
|
|
992
996
|
let min = styleData[at + STYLE_MIN_PX_FIELD]!
|
|
993
997
|
if (min > 0 || styleData[at + STYLE_MAX_PX_FIELD]! > 0) {
|
|
994
998
|
clamped.add(sprite)
|
|
@@ -1005,7 +1009,7 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
1005
1009
|
pipeline: gpu.pipeline,
|
|
1006
1010
|
quad: gpu.quad,
|
|
1007
1011
|
atlases: atlases.map(a => a.texture),
|
|
1008
|
-
buffers: () => [pose, style],
|
|
1012
|
+
buffers: () => [pose, style.buffer],
|
|
1009
1013
|
count: () => published,
|
|
1010
1014
|
tint: () => tint,
|
|
1011
1015
|
pick: (x, y, zoom) => layer.pick(x, y, zoom),
|
|
@@ -1195,7 +1199,7 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
1195
1199
|
spatial.flush()
|
|
1196
1200
|
views.dispose()
|
|
1197
1201
|
destroyBuffer(pose)
|
|
1198
|
-
|
|
1202
|
+
style.destroy()
|
|
1199
1203
|
gpu.dispose()
|
|
1200
1204
|
},
|
|
1201
1205
|
_add(opts) {
|
|
@@ -1257,7 +1261,7 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
1257
1261
|
styleData[at + 1] = g
|
|
1258
1262
|
styleData[at + 2] = b
|
|
1259
1263
|
styleData[at + 3] = width
|
|
1260
|
-
|
|
1264
|
+
style.mark(sprite._slot, sprite._slot + 1)
|
|
1261
1265
|
layer._schedule()
|
|
1262
1266
|
},
|
|
1263
1267
|
_write(sprite, opts) {
|
|
@@ -1287,7 +1291,7 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
1287
1291
|
) {
|
|
1288
1292
|
writeStyle(sprite, opts, atlas)
|
|
1289
1293
|
}
|
|
1290
|
-
if (moved ||
|
|
1294
|
+
if (moved || style.dirty) layer._schedule()
|
|
1291
1295
|
},
|
|
1292
1296
|
_read(sprite) {
|
|
1293
1297
|
let at = sprite._slot * STYLE_FLOATS
|
|
@@ -1354,6 +1358,10 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
1354
1358
|
},
|
|
1355
1359
|
_groups: new Set(),
|
|
1356
1360
|
_root: root,
|
|
1361
|
+
_stream: style,
|
|
1362
|
+
get _records() {
|
|
1363
|
+
return styleData
|
|
1364
|
+
},
|
|
1357
1365
|
}
|
|
1358
1366
|
if (opts?.autoFree !== false && getOwner()) onCleanup(() => layer.dispose())
|
|
1359
1367
|
return layer
|
package/src/records.ts
CHANGED
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
// algorithm, later over earlier; `orderBy` swaps that for a core-produced
|
|
6
6
|
// key order at publish, records untouched); mutations batch, and the flush
|
|
7
7
|
// (ahead of the frame's paint, or at a microtask outside a frame)
|
|
8
|
-
// publishes what changed
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
8
|
+
// publishes what changed through the core's record stream
|
|
9
|
+
// (`createRecordStream` in @solidrt/core/gpu, the publish model every
|
|
10
|
+
// JS-stepped population shares): the dirty record range as one buffer
|
|
11
|
+
// write, or under `orderBy` the drawn prefix whole through the zero-copy
|
|
12
|
+
// write lease (the core gathers it into key order during the copy, and a
|
|
13
|
+
// byte range has no stable position under a permutation). A moved sprite
|
|
14
|
+
// is 20 float stores plus its share of one memcpy per dirty frame; a
|
|
15
|
+
// static layer publishes nothing and therefore costs nothing.
|
|
14
16
|
//
|
|
15
17
|
// The record vocabulary is @solidrt/3d's record mesh's, one dimension
|
|
16
18
|
// down: `records(layer)` is the mirror, `updateRecords(layer, { first?,
|
|
@@ -32,13 +34,13 @@
|
|
|
32
34
|
// (addSprite/setSprite/...) are shared with the node layer; picking here is
|
|
33
35
|
// the JS reverse walk (pointInSprite), since records have no nodes.
|
|
34
36
|
import { getOwner, onBeforeRender, onCleanup, runWithOwner } from "@solidrt/core"
|
|
35
|
-
import {
|
|
37
|
+
import { checkScreenSize, createRecordStream, destroyBuffer, screenSizeScale } from "@solidrt/core/gpu"
|
|
36
38
|
import type { BufferId } from "@solidrt/core/gpu"
|
|
37
39
|
import { checkAtlases, frameIndex } from "./atlas.ts"
|
|
38
40
|
import type { Atlas } from "./atlas.ts"
|
|
39
41
|
import { fullFrame, isFrame, writeFrame } from "./frames.ts"
|
|
40
42
|
import { checkTint, readFrame } from "./layer.ts"
|
|
41
|
-
import type { LayerBase, Sprite, SpriteLayerOptions, SpriteOptions, SpriteState } from "./layer.ts"
|
|
43
|
+
import type { LayerBase, Sprite, SpriteLayer, SpriteLayerOptions, SpriteOptions, SpriteState } from "./layer.ts"
|
|
42
44
|
import { pointInSprite } from "./pick.ts"
|
|
43
45
|
import { createSpritePipeline, INSTANCE_ATTRIBUTES, VERTEX } from "./shaders.ts"
|
|
44
46
|
import { createViews } from "./views.ts"
|
|
@@ -83,10 +85,6 @@ export type RecordLayerOptions = Omit<SpriteLayerOptions, "orderBy"> & {
|
|
|
83
85
|
}
|
|
84
86
|
|
|
85
87
|
export type RecordLayer = LayerBase & {
|
|
86
|
-
/** The record mirror; read it through records(). Replaced by growth. */
|
|
87
|
-
_records: Float32Array
|
|
88
|
-
/** The record range [lo, hi) written since the last publish, or null. */
|
|
89
|
-
_dirty: [number, number] | null
|
|
90
88
|
/** The count dial (setRecordCount): how many leading sprites draw and
|
|
91
89
|
* pick; Infinity (the identity of the min against the live count)
|
|
92
90
|
* until a dial is set. */
|
|
@@ -99,21 +97,34 @@ export type RecordLayer = LayerBase & {
|
|
|
99
97
|
export type UpdateRecordsOptions = { first?: number; count?: number }
|
|
100
98
|
|
|
101
99
|
/**
|
|
102
|
-
* The layer's record mirror - the raw power path.
|
|
103
|
-
* INSTANCE_FLOATS floats: [cx, cy, w, h, u0, v0, u1,
|
|
104
|
-
* tintG, tintB, tintA, minScreenPx, maxScreenPx, atlas,
|
|
105
|
-
* outlineG, outlineB, outlineWidth], record i at i *
|
|
106
|
-
* `atlas` is the frame's texture as its index in the
|
|
107
|
-
* list, and the outline is read only by a
|
|
108
|
-
* `Atlas.sdf`): rgb in 0..1 and a width in
|
|
100
|
+
* The layer's record mirror - the raw power path. On a records layer the
|
|
101
|
+
* layout per sprite is INSTANCE_FLOATS floats: [cx, cy, w, h, u0, v0, u1,
|
|
102
|
+
* v1, rot, tintR, tintG, tintB, tintA, minScreenPx, maxScreenPx, atlas,
|
|
103
|
+
* outlineR, outlineG, outlineB, outlineWidth], record i at i *
|
|
104
|
+
* INSTANCE_FLOATS; `atlas` is the frame's texture as its index in the
|
|
105
|
+
* layer's `atlases` list, and the outline is read only by a
|
|
106
|
+
* distance-field atlas (see `Atlas.sdf`): rgb in 0..1 and a width in
|
|
107
|
+
* world pixels, zero for none. Record order is draw order - unless the
|
|
109
108
|
* layer was created with `orderBy`, which draws in key order while record
|
|
110
|
-
* i keeps meaning sprite i.
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
109
|
+
* i keeps meaning sprite i. Do not cache indices across destroySprite -
|
|
110
|
+
* records shift.
|
|
111
|
+
*
|
|
112
|
+
* On a node layer (createSpriteLayer) the records are its STYLE records,
|
|
113
|
+
* STYLE_FLOATS floats per sprite slot: [u0, v0, u1, v1, tintR, tintG,
|
|
114
|
+
* tintB, tintA, renderOrder, minScreenPx, maxScreenPx, atlas, outlineR,
|
|
115
|
+
* outlineG, outlineB, outlineWidth], slot i at i * STYLE_FLOATS (a
|
|
116
|
+
* sprite's slot is fixed for its life; a freed slot recycles) - the pose
|
|
117
|
+
* lives in the core and is written through setSprite. The 3d
|
|
118
|
+
* `records(instancedMesh)`, one dimension down: a bulk restyle (a palette
|
|
119
|
+
* cycle over thousands of sprites) is one loop over this and one
|
|
120
|
+
* updateRecords.
|
|
121
|
+
*
|
|
122
|
+
* Write fields directly for large per-frame populations, then
|
|
123
|
+
* updateRecords the range. Read it AT USE TIME: an add past capacity
|
|
124
|
+
* replaces the array, and a hoisted reference becomes a dead copy whose
|
|
125
|
+
* writes publish nothing. @solidrt/3d's records(mesh).
|
|
115
126
|
*/
|
|
116
|
-
export function records(layer: RecordLayer): Float32Array {
|
|
127
|
+
export function records(layer: RecordLayer | SpriteLayer): Float32Array {
|
|
117
128
|
return layer._records
|
|
118
129
|
}
|
|
119
130
|
|
|
@@ -123,13 +134,14 @@ export function records(layer: RecordLayer): Float32Array {
|
|
|
123
134
|
* moved records of ten thousand cost ten); the whole mirror by default.
|
|
124
135
|
* Write the mirror first, through records(), then call this. The range
|
|
125
136
|
* is against the mirror's capacity, not the live sprites: a record past
|
|
126
|
-
* the last live sprite
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* permutation. @solidrt/3d's
|
|
137
|
+
* the last live sprite (on a node layer, past the highest slot in use)
|
|
138
|
+
* is accepted and never drawn. Under `orderBy` any range republishes the
|
|
139
|
+
* drawn prefix whole - the core gathers it into key order, and a byte
|
|
140
|
+
* range has no stable position under a permutation. @solidrt/3d's
|
|
141
|
+
* updateRecords(mesh).
|
|
130
142
|
*/
|
|
131
|
-
export function updateRecords(layer: RecordLayer, options: UpdateRecordsOptions = {}): void {
|
|
132
|
-
let capacity = layer.
|
|
143
|
+
export function updateRecords(layer: RecordLayer | SpriteLayer, options: UpdateRecordsOptions = {}): void {
|
|
144
|
+
let capacity = layer._stream.capacity
|
|
133
145
|
let first = options.first ?? 0
|
|
134
146
|
let count = options.count ?? capacity - first
|
|
135
147
|
if (!Number.isInteger(first) || !Number.isInteger(count) || first < 0 || count < 0 || first + count > capacity) {
|
|
@@ -154,14 +166,10 @@ export function setRecordCount(layer: RecordLayer, count: number): void {
|
|
|
154
166
|
layer._schedule()
|
|
155
167
|
}
|
|
156
168
|
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
function markRecords(layer: RecordLayer, lo: number, hi: number): void {
|
|
160
|
-
|
|
161
|
-
else {
|
|
162
|
-
if (lo < layer._dirty[0]) layer._dirty[0] = lo
|
|
163
|
-
if (hi > layer._dirty[1]) layer._dirty[1] = hi
|
|
164
|
-
}
|
|
169
|
+
// Mark records [lo, hi) of the layer's stream and schedule the flush that
|
|
170
|
+
// publishes them.
|
|
171
|
+
function markRecords(layer: RecordLayer | SpriteLayer, lo: number, hi: number): void {
|
|
172
|
+
layer._stream.mark(lo, hi)
|
|
165
173
|
layer._schedule()
|
|
166
174
|
}
|
|
167
175
|
|
|
@@ -179,10 +187,7 @@ export function createRecordLayer(atlases: Atlas[], opts?: RecordLayerOptions):
|
|
|
179
187
|
throw new Error(`createRecordLayer: capacity must be a positive integer, got ${capacity}`)
|
|
180
188
|
}
|
|
181
189
|
let label = opts?.label ?? "sprites"
|
|
182
|
-
let
|
|
183
|
-
label: `${label}-records`,
|
|
184
|
-
autoFree: false,
|
|
185
|
-
})
|
|
190
|
+
let stream = createRecordStream(RECORD_BYTES, capacity, { label: `${label}-records`, autoFree: false })
|
|
186
191
|
let tint = opts?.tint ?? [1, 1, 1, 1]
|
|
187
192
|
checkTint("createRecordLayer", tint)
|
|
188
193
|
let orderBy = opts?.orderBy
|
|
@@ -215,60 +220,45 @@ export function createRecordLayer(atlases: Atlas[], opts?: RecordLayerOptions):
|
|
|
215
220
|
|
|
216
221
|
// How many leading sprites draw: the dial against the live count.
|
|
217
222
|
let drawn = () => Math.min(layer._dial, layer._order.length)
|
|
218
|
-
// Publish the first `n` records whole through the write lease: a
|
|
219
|
-
// replacement buffer's first fill (its contents are unspecified), and
|
|
220
|
-
// every publish of an ordered layer (the gather runs in the copy).
|
|
221
|
-
let publishWhole = (target: BufferId, n: number) => {
|
|
222
|
-
let out = beginBufferWrite(target)
|
|
223
|
-
out.set(layer._records.subarray(0, n * INSTANCE_FLOATS))
|
|
224
|
-
endBufferWrite(target, n * RECORD_BYTES)
|
|
225
|
-
}
|
|
226
223
|
|
|
227
|
-
// The
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
let
|
|
224
|
+
// The buffers growth replaced since the last flush (addSprite grows the
|
|
225
|
+
// stream at once; the views still draw from the first of these until the
|
|
226
|
+
// flush swaps the replacement in). The entries hold a swapped-out buffer
|
|
227
|
+
// alive until the swap lands, so the destroys are safe to issue right
|
|
228
|
+
// after.
|
|
229
|
+
let grownFrom: BufferId[] = []
|
|
233
230
|
let flush = () => {
|
|
234
231
|
scheduled = false
|
|
235
232
|
if (disposed) return
|
|
236
|
-
let dirty = layer._dirty
|
|
237
|
-
layer._dirty = null
|
|
238
|
-
let mirror = layer._records
|
|
239
233
|
let live = layer._order.length
|
|
240
234
|
let count = drawn()
|
|
241
|
-
if (
|
|
242
|
-
gpuCapacity = mirror.length / INSTANCE_FLOATS
|
|
243
|
-
let grown = createBuffer(mirror.length * Float32Array.BYTES_PER_ELEMENT, { label: `${label}-records`, autoFree: false })
|
|
235
|
+
if (grownFrom.length > 0) {
|
|
244
236
|
// The replacement is filled whole before the swap (the entry must
|
|
245
237
|
// never point at an unwritten buffer): the live prefix, or under
|
|
246
238
|
// orderBy the drawn one, which is all the gather may see.
|
|
247
|
-
|
|
248
|
-
views.setBuffers([
|
|
239
|
+
stream.publish(true, ordered ? count : live)
|
|
240
|
+
views.setBuffers([stream.buffer])
|
|
249
241
|
views.setCount(count)
|
|
250
242
|
if (ordered) {
|
|
251
243
|
// That publish landed BEFORE the swap, so the order had not yet
|
|
252
244
|
// followed to the grown buffer and it went out ungathered. One
|
|
253
245
|
// more publish, now under the swapped-in order, restores key
|
|
254
246
|
// order - growth frames only.
|
|
255
|
-
|
|
247
|
+
stream.publish(true, count)
|
|
256
248
|
}
|
|
257
|
-
destroyBuffer(
|
|
258
|
-
|
|
249
|
+
for (let old of grownFrom) destroyBuffer(old)
|
|
250
|
+
grownFrom.length = 0
|
|
259
251
|
published = count
|
|
260
252
|
return
|
|
261
253
|
}
|
|
262
254
|
if (ordered) {
|
|
263
255
|
// The gathered set IS the drawn prefix: a dirty record or a moved
|
|
264
256
|
// dial republishes it whole.
|
|
265
|
-
if (dirty
|
|
266
|
-
} else
|
|
257
|
+
if (stream.dirty || count !== published) stream.publish(true, count)
|
|
258
|
+
} else {
|
|
267
259
|
// The dirty range, clipped to the live sprites (a record past them
|
|
268
260
|
// is never drawn): one partial write at its byte offset.
|
|
269
|
-
|
|
270
|
-
let hi = Math.min(dirty[1], live)
|
|
271
|
-
if (hi > lo) writeBuffer(buffer, mirror.subarray(lo * INSTANCE_FLOATS, hi * INSTANCE_FLOATS), lo * RECORD_BYTES)
|
|
261
|
+
stream.publish(false, live)
|
|
272
262
|
}
|
|
273
263
|
if (count !== published) {
|
|
274
264
|
views.setCount(count)
|
|
@@ -335,7 +325,7 @@ export function createRecordLayer(atlases: Atlas[], opts?: RecordLayerOptions):
|
|
|
335
325
|
pipeline: gpu.pipeline,
|
|
336
326
|
quad: gpu.quad,
|
|
337
327
|
atlases: atlases.map(a => a.texture),
|
|
338
|
-
buffers: () => [buffer],
|
|
328
|
+
buffers: () => [stream.buffer],
|
|
339
329
|
count: () => published,
|
|
340
330
|
tint: () => tint,
|
|
341
331
|
pick: (x, y, zoom) => layer.pick(x, y, zoom),
|
|
@@ -383,7 +373,9 @@ export function createRecordLayer(atlases: Atlas[], opts?: RecordLayerOptions):
|
|
|
383
373
|
for (let sprite of layer._order) sprite.layer = null
|
|
384
374
|
layer._order.length = 0
|
|
385
375
|
views.dispose()
|
|
386
|
-
destroyBuffer(
|
|
376
|
+
for (let old of grownFrom) destroyBuffer(old)
|
|
377
|
+
grownFrom.length = 0
|
|
378
|
+
stream.destroy()
|
|
387
379
|
gpu.dispose()
|
|
388
380
|
},
|
|
389
381
|
_add(opts) {
|
|
@@ -407,12 +399,11 @@ export function createRecordLayer(atlases: Atlas[], opts?: RecordLayerOptions):
|
|
|
407
399
|
}
|
|
408
400
|
let atlas = checkWrite("addSprite", record)
|
|
409
401
|
let index = layer._order.length
|
|
410
|
-
if (
|
|
411
|
-
// Growth replaces the mirror (doubling); the flush
|
|
412
|
-
//
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
layer._records = next
|
|
402
|
+
if (index + 1 > stream.capacity) {
|
|
403
|
+
// Growth replaces the mirror and the buffer (doubling); the flush
|
|
404
|
+
// fills the replacement whole and swaps it in.
|
|
405
|
+
grownFrom.push(stream.grow(stream.capacity * 2))
|
|
406
|
+
layer._records = new Float32Array(stream.bytes.buffer)
|
|
416
407
|
}
|
|
417
408
|
let sprite: SpriteState = { layer, node: null, _slot: index, _x: 0, _y: 0, _w: 0, _h: 0, _rot: 0, _flipX: false, _flipY: false, _visible: true, _parent: null }
|
|
418
409
|
layer._order.push(sprite)
|
|
@@ -469,8 +460,8 @@ export function createRecordLayer(atlases: Atlas[], opts?: RecordLayerOptions):
|
|
|
469
460
|
scheduled = true
|
|
470
461
|
RESOLVED.then(flush)
|
|
471
462
|
},
|
|
472
|
-
|
|
473
|
-
|
|
463
|
+
_stream: stream,
|
|
464
|
+
_records: new Float32Array(stream.bytes.buffer),
|
|
474
465
|
_dial: Infinity,
|
|
475
466
|
_order: [],
|
|
476
467
|
}
|
package/src/text.ts
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
//
|
|
10
10
|
// The node layer only: a run is a group. Three verbs like the sprites':
|
|
11
11
|
// addText, setText (re-laid out, the sprite pool reused), destroyText.
|
|
12
|
+
// A setText that changes only the paint (tint, outline, renderOrder)
|
|
13
|
+
// re-writes the glyph sprites without shaping: a fade is sprite writes,
|
|
14
|
+
// not an engine call per frame.
|
|
12
15
|
import { addGroup, addSprite, destroyGroup, destroySprite, setGroup, setSprite } from "./layer.ts"
|
|
13
16
|
import type { Sprite, SpriteGroup, SpriteLayer } from "./layer.ts"
|
|
14
17
|
import type { SpriteFont } from "./font.ts"
|
|
@@ -119,7 +122,9 @@ export function addText(layer: SpriteLayer, opts: AddTextOptions): TextRun {
|
|
|
119
122
|
}
|
|
120
123
|
|
|
121
124
|
/** Change a run: the style fields re-lay it out (the sprite pool is
|
|
122
|
-
* reused), the pose fields move its group. Absent keys keep values
|
|
125
|
+
* reused), the pose fields move its group. Absent keys keep values, and so
|
|
126
|
+
* does a key passed with its current value; a change to only tint,
|
|
127
|
+
* outline or renderOrder re-writes the glyph sprites without shaping. */
|
|
123
128
|
export function setText(run: TextRun, opts: Partial<TextStyle> & TextPose): void {
|
|
124
129
|
let state = run as RunState
|
|
125
130
|
if (state.group.layer === null) return
|
|
@@ -127,12 +132,42 @@ export function setText(run: TextRun, opts: Partial<TextStyle> & TextPose): void
|
|
|
127
132
|
if (x !== undefined || y !== undefined || rotation !== undefined || visible !== undefined) {
|
|
128
133
|
setGroup(state.group, { x, y, rotation, visible })
|
|
129
134
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
+
// Compare by value, not by presence: a component effect passes every
|
|
136
|
+
// style key on any change, and only a key that differs costs anything.
|
|
137
|
+
let current = state._style
|
|
138
|
+
let next: TextStyle = { ...current }
|
|
139
|
+
let reshape = false
|
|
140
|
+
let repaint = false
|
|
141
|
+
for (let k of Object.keys(style) as (keyof TextStyle)[]) {
|
|
142
|
+
let value = style[k]
|
|
143
|
+
if (value === undefined || sameStyleValue(k, value, current[k])) continue
|
|
144
|
+
;(next as Record<string, unknown>)[k] = value
|
|
145
|
+
if (PAINT_KEYS.has(k)) repaint = true
|
|
146
|
+
else reshape = true
|
|
135
147
|
}
|
|
148
|
+
if (reshape) relayout(state, next)
|
|
149
|
+
else if (repaint) {
|
|
150
|
+
checkStyle(next)
|
|
151
|
+
state._style = next
|
|
152
|
+
paint(state)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// The style keys that change only how a glyph sprite is drawn, never which
|
|
157
|
+
// glyphs or where: every other key re-shapes.
|
|
158
|
+
const PAINT_KEYS: ReadonlySet<keyof TextStyle> = new Set<keyof TextStyle>(["tint", "outline", "renderOrder"])
|
|
159
|
+
|
|
160
|
+
// Equal as a style value: tint by channel, outline by colour and width,
|
|
161
|
+
// the rest by identity.
|
|
162
|
+
function sameStyleValue(key: keyof TextStyle, a: unknown, b: unknown): boolean {
|
|
163
|
+
if (a === b) return true
|
|
164
|
+
if (key === "tint" && Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((c, i) => c === b[i])
|
|
165
|
+
if (key === "outline" && a != null && b != null) {
|
|
166
|
+
let { color: ca, width: wa } = a as NonNullable<TextStyle["outline"]>
|
|
167
|
+
let { color: cb, width: wb } = b as NonNullable<TextStyle["outline"]>
|
|
168
|
+
return wa === wb && ca[0] === cb[0] && ca[1] === cb[1] && ca[2] === cb[2]
|
|
169
|
+
}
|
|
170
|
+
return false
|
|
136
171
|
}
|
|
137
172
|
|
|
138
173
|
/** Remove the run: its group and every glyph sprite. */
|
|
@@ -164,6 +199,27 @@ function relayout(run: RunState, style: TextStyle): void {
|
|
|
164
199
|
place(run)
|
|
165
200
|
}
|
|
166
201
|
|
|
202
|
+
// The paint fields of the run's style, as every glyph sprite takes them.
|
|
203
|
+
function paintOf(style: TextStyle) {
|
|
204
|
+
return { tint: style.tint ?? [1, 1, 1, 1], outline: style.outline ?? null, renderOrder: style.renderOrder ?? 0 }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Write a sprite's outline from the style (none clears it).
|
|
208
|
+
function writeOutline(layer: SpriteLayer, sprite: Sprite, outline: TextStyle["outline"]): void {
|
|
209
|
+
if (outline != null) layer._outline(sprite, outline.color[0], outline.color[1], outline.color[2], outline.width)
|
|
210
|
+
else layer._outline(sprite, 0, 0, 0, 0)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Re-paint the glyph sprites in place: the layout, the pool and the
|
|
214
|
+
// pending cells all stand, only tint, outline and order are written.
|
|
215
|
+
function paint(run: RunState): void {
|
|
216
|
+
let { tint, outline, renderOrder } = paintOf(run._style)
|
|
217
|
+
for (let sprite of run.sprites) {
|
|
218
|
+
setSprite(sprite, { tint, renderOrder })
|
|
219
|
+
writeOutline(run.layer, sprite, outline)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
167
223
|
// Write every glyph sprite from the layout and the font's cells: a cell
|
|
168
224
|
// the atlas lacks leaves its sprite hidden and asks the font for it.
|
|
169
225
|
function place(run: RunState): void {
|
|
@@ -172,9 +228,7 @@ function place(run: RunState): void {
|
|
|
172
228
|
let fontSize = style.fontSize ?? font.fontSize
|
|
173
229
|
// Cells are made at `font.size` texels per em; the run draws at fontSize.
|
|
174
230
|
let scale = fontSize / font.size
|
|
175
|
-
let
|
|
176
|
-
let outline = style.outline ?? null
|
|
177
|
-
let renderOrder = style.renderOrder ?? 0
|
|
231
|
+
let { tint, outline, renderOrder } = paintOf(style)
|
|
178
232
|
let placements = run._layout.glyphs
|
|
179
233
|
run._pending.clear()
|
|
180
234
|
let missing: number[] = []
|
|
@@ -210,8 +264,7 @@ function place(run: RunState): void {
|
|
|
210
264
|
} else {
|
|
211
265
|
setSprite(sprite, fields)
|
|
212
266
|
}
|
|
213
|
-
|
|
214
|
-
else layer._outline(sprite, 0, 0, 0, 0)
|
|
267
|
+
writeOutline(layer, sprite, outline)
|
|
215
268
|
used++
|
|
216
269
|
}
|
|
217
270
|
for (let i = used; i < sprites.length; i++) destroySprite(sprites[i]!)
|