Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion packages/presentation/particle/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,10 @@ export function decodeParticleRenderOutput(
const bounds = boundsState === 0 ? null : Object.freeze({ minimum, maximum })
const output: ParticleRenderItem[] = new Array(count)
const sheets = new SheetImagesCache(view)
// Consecutive particles commonly belong to one PCF system (notably weather).
// Its immutable UUID does not need sixteen new string concatenations for
// every particle. Compare all 128 bits; keep only this packet's last identity.
let identityOffset = -1, systemUuid = ""
Comment on lines +180 to +183
for (let index = 0; index < count; index += 1) {
const offset = OUTPUT_HEADER_BYTES + index * OUTPUT_RECORD_BYTES
const primitive = bytes[offset + 14]
Expand Down Expand Up @@ -261,14 +265,23 @@ export function decodeParticleRenderOutput(
) {
throw new ParticleAdapterError("MalformedOutput", "particle output contains an invalid scalar")
}
const uuidOffset = offset + 16
if (identityOffset < 0
|| view.getUint32(uuidOffset, true) !== view.getUint32(identityOffset, true)
|| view.getUint32(uuidOffset + 4, true) !== view.getUint32(identityOffset + 4, true)
|| view.getUint32(uuidOffset + 8, true) !== view.getUint32(identityOffset + 8, true)
|| view.getUint32(uuidOffset + 12, true) !== view.getUint32(identityOffset + 12, true)) {
systemUuid = uuid(bytes, uuidOffset)
identityOffset = uuidOffset
}
output[index] = Object.freeze({
identity: view.getUint32(offset, true),
effectIdentity: view.getUint32(offset + 4, true),
particleIdentity: view.getUint32(offset + 8, true),
rendererIndex: view.getUint16(offset + 12, true),
primitive: primitive === 0 ? "sprite" : primitive === 1 ? "trail" : "rope",
sky: (bytes[offset + 15]! & 1) !== 0,
systemUuid: uuid(bytes, offset + 16),
systemUuid,
material,
position,
previousPosition,
Expand Down
29 changes: 29 additions & 0 deletions packages/presentation/particle/tests/particle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,35 @@ test("repeated sheet rectangles share frozen values without borrowing packet byt
expect(first!.next[0]![0]).toBe(retained)
})

test("packet-local UUID reuse preserves all 128 bits, alternating systems and retained values", () => {
const bytes = output({ count: 18 })
for (let index = 1; index <= 16; index++) bytes[40 + index * 436 + 16 + index - 1] = index
const expected = Array.from({ length: 18 }, (_, index) => Buffer.from(bytes.subarray(40 + index * 436 + 16, 40 + index * 436 + 32)).toString("hex"))
const retained = decodeParticleRenderOutput(bytes, ["smoke"])
expect(retained.items.map(item => item.systemUuid)).toEqual(expected)
bytes.fill(0xff, 40 + 16, 40 + 32)
expect(retained.items.map(item => item.systemUuid)).toEqual(expected)
expect(decodeParticleRenderOutput(bytes, ["smoke"]).items[0]!.systemUuid).toBe("ff".repeat(16))
})

test("repeated UUID runs preserve complete items and do not borrow an unaligned packet", () => {
const single = decodeParticleRenderOutput(output(), ["smoke"]).items[0]!
const packet = output({ count: 7 })
const storage = new Uint8Array(packet.length + 3)
storage.set(packet, 3)
const bytes = storage.subarray(3)
const systems = [0xab, 0xab, 0xab, 0xcd, 0xcd, 0xab, 0xab]
systems.forEach((value, index) => bytes.fill(value, 40 + index * 436 + 16, 40 + index * 436 + 32))
const expected = systems.map((value, index) => ({ ...single, identity: index + 1, systemUuid: value.toString(16).repeat(16) }))
const decoded = decodeParticleRenderOutput(bytes, ["smoke"])
expect(decoded.items).toEqual(expected)
expect(decoded.items.every(Object.isFrozen)).toBe(true)
bytes.fill(0)
expect(decoded.items).toEqual(expected)
expect(decodeParticleRenderOutput(output({ count: 0 }), ["smoke"]).items).toEqual([])
expect(decodeParticleRenderOutput(output(), ["smoke"]).items[0]).toEqual(single)
})

test("sheet cache bounds do not omit unique rectangles or accept a nonfinite late record", () => {
const bytes = output({ count: 600 }), view = new DataView(bytes.buffer)
for (let index = 0; index < 600; index++) view.setFloat32(40 + index * 436 + 132, index, true)
Expand Down