Skip to content

Commit 23dd003

Browse files
dalkiaclaude
andcommitted
feat: spheres-only benchmark with large batches
Drop the in-scene box control group — the comparison is this scene on an old Explorer build (no mesh sharing) vs a new one (shared mesh). Spheres only, ADD_BATCH raised to 1000, footprint widened to span the parcels and stack upward. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 5b05ca5 commit 23dd003

3 files changed

Lines changed: 84 additions & 132 deletions

File tree

scenes/40,40-sphere-mesh-sharing-benchmark/README.md

Lines changed: 35 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,50 +4,44 @@
44

55
This scene validates a Unity Explorer memory optimization: `SpherePrimitive`
66
now shares **one immutable `Mesh` asset** across every sphere entity in the
7-
scene, instead of allocating a brand new `Mesh` per entity. `Box` / `Plane` /
8-
`Cylinder` primitives are unaffected — they still allocate a distinct mesh
9-
per entity.
7+
scene, instead of allocating a brand new `Mesh` per entity.
108

11-
The scene starts **empty** and gives you an on-screen panel to spawn shapes in
12-
real time, so you can watch the Memory Profiler react as you add each batch:
9+
The comparison is made **across Explorer builds**, not within the scene: run
10+
this same scene on an **old build** (no sharing) and on a **new build** (shared
11+
mesh) and watch the Memory Profiler in each. There is deliberately no in-scene
12+
control group — the old build *is* the control.
1313

14-
- **`+N spheres`** — spawns a batch of sphere entities (`MeshRenderer.setSphere`)
15-
in the **left half** (blue) — the **optimized** path.
16-
- **`+N boxes`** — spawns a batch of box entities (`MeshRenderer.setBox`) in the
17-
**right half** (red) — the **control/unoptimized** path.
18-
- **`Delete all`** — removes every spawned shape so you can start over.
14+
The scene starts **empty** and gives you an on-screen panel to pile on spheres
15+
in real time:
1916

20-
The panel also shows the live sphere and box counts. Adding spheres leaves mesh
21-
memory flat; adding boxes climbs it 1:1 — the difference is attributable to the
22-
mesh allocation strategy, not scene layout (both use the same spacing and scale).
17+
- **`+N spheres`** — spawns a batch of sphere entities (`MeshRenderer.setSphere`).
18+
- **`Delete all`** — removes every spawned sphere so you can start over.
2319

24-
Each button press spawns `ADD_BATCH` entities (default `100`). To change the
20+
Each button press spawns `ADD_BATCH` spheres (default `1000`). To change the
2521
batch size, edit the single constant near the top of
2622
[`src/spawner.ts`](src/spawner.ts):
2723

2824
```ts
29-
export const ADD_BATCH = 100
25+
export const ADD_BATCH = 1000
3026
```
3127

3228
## Code layout
3329

3430
- [`src/index.ts`](src/index.ts) — entry point; just wires up the UI.
3531
- [`src/spawner.ts`](src/spawner.ts) — all spawn/registry logic (`addSpheres`,
36-
`addBoxes`, `deleteAll`, live counts) and the layout constants.
37-
- [`src/ui.tsx`](src/ui.tsx) — the on-screen panel with the buttons and counters.
32+
`deleteAll`, live count) and the layout constants.
33+
- [`src/ui.tsx`](src/ui.tsx) — the on-screen panel with the buttons and counter.
3834

3935
## Scene layout
4036

4137
- Parcels: `40,40` (base) and `41,40` — a 2-parcel-wide, 1-parcel-deep strip
42-
(32m × 16m in local coordinates), large enough to hold both groups side by
43-
side.
44-
- Spheres fill the **left half**, anchored at local `x: 2, z: 2`.
45-
- Boxes fill the **right half**, anchored at local `x: 17.4, z: 2`.
46-
- Each group grows a grid `GRID_COLUMNS = 20` wide with 0.6m spacing between
47-
entity centers and a 0.4m entity scale, wrapping to a new row (`+z`) as more
48-
are added. At the default parcel depth that's ~26 rows before a group runs
49-
past the parcel edge; add parcels or reduce `SPACING` if you plan to spawn
50-
many thousands per group.
38+
(32m × 16m in local coordinates).
39+
- Spheres fill a fixed `GRID_COLUMNS × GRID_DEPTH` footprint (48 × 24) anchored
40+
at local `x: 2, z: 1`, with 0.6m spacing and a 0.4m entity scale so spheres
41+
never touch.
42+
- Once a layer fills (`48 × 24 = 1152` spheres), the stack grows **upward**
43+
(`+y`) into the next layer. The 2-parcel scene height limit (~31m) allows
44+
roughly 50 layers before the top starts clipping.
5145

5246
## How to run / preview
5347

@@ -58,23 +52,21 @@ npm install
5852
npm run start
5953
```
6054

61-
Or open this folder in VS Code with the Decentraland Editor extension and
62-
press **Run Scene**.
55+
Then open `http://127.0.0.1:8000?position=40,40` in a browser, or use the
56+
desktop-client link the server prints. Or open this folder in VS Code with the
57+
Decentraland Editor extension and press **Run Scene**.
6358

6459
## How to validate in the Unity Explorer Profiler
6560

66-
1. Launch the Unity Explorer and preview this scene (point it at the local
67-
preview server started by `npm run start`, or deploy to a test catalyst).
68-
2. Once the scene has loaded, open the **Profiler** window and switch to the
69-
**Memory** module. Keep it visible next to the scene.
70-
3. Click **`+N spheres`** several times and watch the **Mesh** asset category:
71-
the sphere group should stay at a **single shared Mesh asset**, and mesh
72-
memory should stay flat no matter how many spheres you add.
73-
4. Now click **`+N boxes`** several times: the box group should show a **new
74-
distinct Mesh asset per entity**, with total mesh memory climbing linearly
75-
with the box count.
76-
5. Click **`Delete all`** and confirm both counts drop to zero and the
77-
corresponding mesh memory is released.
78-
79-
That contrast — spheres staying flat while boxes climb 1:1, live as you click —
80-
is the proof that the optimization is working.
61+
1. Load this scene on the **old** Explorer build. Open the **Profiler**
62+
**Memory** module and note the **Mesh** asset category.
63+
2. Click **`+N spheres`** several times. On the old build, the Mesh asset count
64+
and total mesh memory climb **1:1 with the sphere count**.
65+
3. Click **`Delete all`** and confirm the count and memory drop back down.
66+
4. Now load the exact same scene on the **new** Explorer build and repeat.
67+
5. This time the Mesh asset category should stay at a **single shared Mesh
68+
asset**, and mesh memory should stay **flat** no matter how many spheres you
69+
spawn.
70+
71+
That difference between the two builds — mesh memory climbing on the old build
72+
vs. staying flat on the new one — is the proof that the optimization is working.

scenes/40,40-sphere-mesh-sharing-benchmark/src/spawner.ts

Lines changed: 37 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -4,92 +4,79 @@ import { Color4 } from '@dcl/sdk/math'
44
/**
55
* Sphere mesh-sharing benchmark — runtime spawner
66
* ------------------------------------------------------------------
7-
* Validates a Unity Explorer memory optimization: SpherePrimitive shares
8-
* ONE immutable Mesh asset across every sphere entity, instead of allocating
9-
* a new Mesh per entity (Box/Plane/Cylinder still allocate a distinct mesh
10-
* per entity).
7+
* Validates a Unity Explorer memory optimization: SpherePrimitive shares ONE
8+
* immutable Mesh asset across every sphere entity, instead of allocating a new
9+
* Mesh per entity.
1110
*
12-
* The UI (see ui.tsx) drives this module in real time so the effect is
13-
* observable live in the Explorer's Memory Profiler: click "Add spheres"
14-
* repeatedly and mesh memory stays flat (shared mesh); click "Add boxes" and
15-
* mesh memory climbs 1:1 with the box count.
11+
* The comparison is made ACROSS Explorer builds: run this same scene on an old
12+
* build (no sharing) and on a new build (shared mesh) and watch the Memory
13+
* Profiler. On the old build, mesh memory climbs 1:1 with the sphere count; on
14+
* the new build it stays flat no matter how many spheres you spawn. That's why
15+
* there's no in-scene control group — the control is the old build itself.
16+
*
17+
* The UI (see ui.tsx) drives this module in real time: click "Add spheres" to
18+
* pile on thousands of spheres, "Delete all" to reset.
1619
*/
1720

18-
// How many entities each button press spawns. Small enough to click through
19-
// several batches while watching the Profiler, large enough that a single box
20-
// batch is visible in the memory graph.
21-
export const ADD_BATCH = 100
21+
// How many spheres each button press spawns. Large so the mesh-memory
22+
// divergence between old and new builds becomes obvious after just a click or
23+
// two.
24+
export const ADD_BATCH = 1000
2225

23-
// Stack layout: each group fills a fixed GRID_COLUMNS x GRID_DEPTH footprint and
24-
// grows UPWARD (+y), layer by layer. GRID_COLUMNS * GRID_DEPTH is sized to equal
25-
// ADD_BATCH, so every button press drops one complete new layer on top of the
26-
// previous one — the stack visibly rises with each click.
27-
const GRID_COLUMNS = 10 // entities along x
28-
const GRID_DEPTH = 10 // entities along z (COLUMNS * DEPTH === ADD_BATCH === one layer)
26+
// Stack layout: spheres fill a fixed GRID_COLUMNS x GRID_DEPTH footprint (sized
27+
// to span most of the 2-parcel scene) and grow UPWARD (+y), layer by layer, as
28+
// more are added.
29+
const GRID_COLUMNS = 48 // entities along x
30+
const GRID_DEPTH = 24 // entities along z
2931
const PER_LAYER = GRID_COLUMNS * GRID_DEPTH
3032
const SPACING = 0.6 // meters between entity centers
31-
const ENTITY_SCALE = 0.4 // meters (diameter/side length) — smaller than SPACING so entities never touch
33+
const ENTITY_SCALE = 0.4 // meters (diameter) — smaller than SPACING so spheres never touch
3234
const BASE_HEIGHT = 0.5 // meters, y of the bottom layer
3335

34-
// Local-coordinate anchors for each grid's near corner. Scene base parcel is
36+
// Local-coordinate anchor for the stack's near corner. Scene base parcel is
3537
// "40,40" with a second parcel at "41,40" (see scene.json), giving a local
36-
// coordinate space of x: 0..32, z: 0..16. Spheres fill the left half, boxes the
37-
// right half.
38-
const SPHERE_GRID_ORIGIN = { x: 2, z: 2 }
39-
const BOX_GRID_ORIGIN = { x: 17.4, z: 2 }
38+
// coordinate space of x: 0..32, z: 0..16. The footprint above fits inside it.
39+
const GRID_ORIGIN = { x: 2, z: 1 }
4040

41-
const SPHERE_COLOR = Color4.create(0.15, 0.35, 0.95, 1) // blue = optimized/shared-mesh path
42-
const BOX_COLOR = Color4.create(0.9, 0.2, 0.15, 1) // red = control/distinct-mesh path
41+
const SPHERE_COLOR = Color4.create(0.15, 0.35, 0.95, 1) // blue
4342

44-
// Live registries of every entity we've spawned, so "Delete all" can remove
45-
// them and the UI can display current counts.
43+
// Live registry of every sphere we've spawned, so "Delete all" can remove them
44+
// and the UI can display the current count.
4645
const spheres: Entity[] = []
47-
const boxes: Entity[] = []
4846

4947
export function getSphereCount(): number {
5048
return spheres.length
5149
}
5250

53-
export function getBoxCount(): number {
54-
return boxes.length
55-
}
56-
5751
export function addSpheres(count: number = ADD_BATCH) {
5852
for (let i = 0; i < count; i++) {
59-
spheres.push(createSphere(stackPosition(SPHERE_GRID_ORIGIN, spheres.length)))
60-
}
61-
}
62-
63-
export function addBoxes(count: number = ADD_BATCH) {
64-
for (let i = 0; i < count; i++) {
65-
boxes.push(createBox(stackPosition(BOX_GRID_ORIGIN, boxes.length)))
53+
spheres.push(createSphere(stackPosition(spheres.length)))
6654
}
6755
}
6856

6957
export function deleteAll() {
7058
for (const entity of spheres) engine.removeEntity(entity)
71-
for (const entity of boxes) engine.removeEntity(entity)
7259
spheres.length = 0
73-
boxes.length = 0
7460
}
7561

76-
// Places the entity at index `i` within a stack anchored at `origin`. Each layer
77-
// is a GRID_COLUMNS x GRID_DEPTH slab in the x/z plane; once a layer fills, the
78-
// stack grows upward (+y) into the next layer.
79-
function stackPosition(origin: { x: number; z: number }, i: number): { x: number; y: number; z: number } {
62+
// Places the sphere at index `i` within the stack. Each layer is a
63+
// GRID_COLUMNS x GRID_DEPTH slab in the x/z plane; once a layer fills, the stack
64+
// grows upward (+y) into the next layer.
65+
function stackPosition(i: number): { x: number; y: number; z: number } {
8066
const layer = Math.floor(i / PER_LAYER)
8167
const withinLayer = i % PER_LAYER
8268
const column = withinLayer % GRID_COLUMNS
8369
const row = Math.floor(withinLayer / GRID_COLUMNS)
8470
return {
85-
x: origin.x + column * SPACING,
71+
x: GRID_ORIGIN.x + column * SPACING,
8672
y: BASE_HEIGHT + layer * SPACING,
87-
z: origin.z + row * SPACING
73+
z: GRID_ORIGIN.z + row * SPACING
8874
}
8975
}
9076

91-
// Optimized path: MeshRenderer.setSphere reuses a single shared, immutable Mesh
92-
// asset across every sphere entity instead of allocating its own.
77+
// MeshRenderer.setSphere reuses a single shared, immutable Mesh asset across
78+
// every sphere entity (on builds with the optimization) instead of allocating
79+
// its own.
9380
function createSphere(position: { x: number; y: number; z: number }): Entity {
9481
const entity = engine.addEntity()
9582
Transform.create(entity, {
@@ -100,16 +87,3 @@ function createSphere(position: { x: number; y: number; z: number }): Entity {
10087
Material.setPbrMaterial(entity, { albedoColor: SPHERE_COLOR, roughness: 0.6 })
10188
return entity
10289
}
103-
104-
// Control/unoptimized path: MeshRenderer.setBox allocates a distinct Mesh asset
105-
// per entity, so this group's Mesh count scales 1:1 with the box count.
106-
function createBox(position: { x: number; y: number; z: number }): Entity {
107-
const entity = engine.addEntity()
108-
Transform.create(entity, {
109-
position,
110-
scale: { x: ENTITY_SCALE, y: ENTITY_SCALE, z: ENTITY_SCALE }
111-
})
112-
MeshRenderer.setBox(entity)
113-
Material.setPbrMaterial(entity, { albedoColor: BOX_COLOR, roughness: 0.6 })
114-
return entity
115-
}

scenes/40,40-sphere-mesh-sharing-benchmark/src/ui.tsx

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,26 @@
11
import { Color4 } from '@dcl/sdk/math'
22
import ReactEcs, { Button, Label, ReactEcsRenderer, UiEntity } from '@dcl/sdk/react-ecs'
3-
import { ADD_BATCH, addBoxes, addSpheres, deleteAll, getBoxCount, getSphereCount } from './spawner'
3+
import { ADD_BATCH, addSpheres, deleteAll, getSphereCount } from './spawner'
44

55
export function setupUi() {
66
ReactEcsRenderer.setUiRenderer(uiComponent)
77
}
88

99
const uiComponent = () => (
10-
// Full-screen wrapper: pin the panel to the vertical middle of the right edge.
10+
// Full-screen wrapper; the panel itself is absolutely positioned against the
11+
// right edge, 25% down from the top of the screen.
1112
<UiEntity
1213
uiTransform={{
1314
width: '100%',
14-
height: '100%',
15-
flexDirection: 'row',
16-
alignItems: 'center', // vertical center → "mid"
17-
justifyContent: 'flex-end' // right edge
15+
height: '100%'
1816
}}
1917
>
2018
<UiEntity
2119
uiTransform={{
2220
width: 520,
23-
height: 620,
24-
margin: '0 32px 0 0',
21+
height: 460,
22+
positionType: 'absolute',
23+
position: { top: '25%', right: 32 },
2524
padding: 20,
2625
flexDirection: 'column',
2726
alignItems: 'center',
@@ -30,22 +29,16 @@ const uiComponent = () => (
3029
uiBackground={{ color: Color4.create(0, 0, 0, 0.6) }}
3130
>
3231
<Label
33-
value="Mesh-sharing benchmark"
34-
fontSize={34}
32+
value="Sphere mesh-sharing benchmark"
33+
fontSize={32}
3534
color={Color4.White()}
3635
uiTransform={{ width: '100%', height: 50, margin: '0 0 8px 0' }}
3736
/>
3837
<Label
39-
value={`Spheres: ${getSphereCount()} (shared mesh)`}
40-
fontSize={26}
38+
value={`Spheres: ${getSphereCount()}`}
39+
fontSize={28}
4140
color={Color4.create(0.4, 0.6, 1, 1)}
42-
uiTransform={{ width: '100%', height: 40 }}
43-
/>
44-
<Label
45-
value={`Boxes: ${getBoxCount()} (mesh per entity)`}
46-
fontSize={26}
47-
color={Color4.create(1, 0.4, 0.35, 1)}
48-
uiTransform={{ width: '100%', height: 40, margin: '0 0 12px 0' }}
41+
uiTransform={{ width: '100%', height: 44, margin: '0 0 12px 0' }}
4942
/>
5043
<Button
5144
uiTransform={{ width: '100%', height: 90, margin: '8px 0' }}
@@ -54,13 +47,6 @@ const uiComponent = () => (
5447
fontSize={30}
5548
onMouseDown={() => addSpheres()}
5649
/>
57-
<Button
58-
uiTransform={{ width: '100%', height: 90, margin: '8px 0' }}
59-
value={`+${ADD_BATCH} boxes`}
60-
variant="primary"
61-
fontSize={30}
62-
onMouseDown={() => addBoxes()}
63-
/>
6450
<Button
6551
uiTransform={{ width: '100%', height: 90, margin: '8px 0' }}
6652
value="Delete all"

0 commit comments

Comments
 (0)