Performance
How willow draws a frame
Each frame willow walks the tree and records a draw command per visible node. It then sorts the commands (render layer, then zOrder, then tree order) and merges neighbours that share a batch key into one GPU draw. The key is the render target, the shader, the blend mode and the texture page. Transforms are only recomputed for nodes that moved, or whose ancestor moved.
So the cheapest scene is one where many nodes in a row share a texture and a blend mode.
What helps
| Do this | Because |
|---|---|
Put sprites that draw together on one texture page. Use an Atlas or TextureRegion.grid on a shared sheet. | Neighbouring sprites from the same page merge into one draw. Alternating between two images breaks the batch every time. |
| Keep blend modes grouped. | A change of blend mode starts a new batch. Put all "add" effects on their own layer. |
Use SpriteBatch for thousands of simple sprites. | Each element is a few numbers, not a node: no transform walk, no hit testing, no children. See Sprites and textures. |
Use TileLayer for grids, under a TilemapViewport for big maps. | One batch per layer, and only on-screen tiles are built. See Tilemaps. |
| Cache still subtrees. | See below. |
Turn on camera culling (engine.camera.cullEnabled = true) for big worlds. | Nodes outside the view are skipped. |
Prefer draw filters. ColorMatrixFilter, PaletteFilter, the pixel-perfect filters and CustomDrawShaderFilter on a single sprite apply during the normal draw. | BlurFilter, OutlineFilter and CustomShaderFilter render to an offscreen texture first. |
| Dispose what you are done with. | dispose() frees a node, texture or font now instead of waiting for the collector. |
Hide instead of rebuild. visible = false skips a subtree. | Re-creating nodes and reloading textures every time a menu opens costs more than keeping them. |
Cache a still subtree
setCacheAsTree(true) records a subtree's draw commands and replays them on later frames instead of walking it again. Moving the cached node, panning the camera and changing its alpha do not break the cache.
ts
import { Node, Sprite, Texture, TextureRegion, CacheTreeMode } from "willow";
class Backdrop {
static main(): void {
const sheet: Texture = Texture.load("assets/props.png");
const props: Array<TextureRegion> = TextureRegion.grid(sheet, 32, 32);
const scenery: Node = new Node("scenery");
for (let i: int = 0; i < 400; i++) {
const s: Sprite = new Sprite(props[i % props.length]);
s.setPosition((i * 53) % 1600, (i * 97) % 600);
scenery.add(s);
}
engine.root2d.add(scenery);
scenery.setCacheAsTree(true, CacheTreeMode.MANUAL);
App.onFrame((dt: float): void => {
if (Keyboard.isPressed(Key.R)) {
const first: Node = scenery.children()[0];
first.visible = !first.visible;
scenery.invalidateCache();
}
});
}
}| Mode | When the cache rebuilds | Cost |
|---|---|---|
CacheTreeMode.MANUAL (default) | Only when you call invalidateCache(). | Nothing extra per change. Best for big static scenery. |
CacheTreeMode.AUTO | Whenever a field changes on the node or below it. | A small cost on every field write in the subtree. |
Cache subtrees that rarely change. A subtree that changes every frame gains nothing and pays for re-recording. Caching needs a real renderer; it does nothing without a window.
Keep still and moving things apart
A cache belongs to one node and covers everything below it. One moving child is enough to make an AUTO cache re-record, or to make a MANUAL cache need an invalidateCache(). So split the tree by how things change, not only by what they are:
text
level
├── scenery setCacheAsTree(true) tiles, props, background: never move
└── actors player, enemies, bullets: move every frameThe camera can pan over scenery and the cache still holds, because moving the cached node or the camera does not break it.
Measure
App.actualFps and App.actualTps report the measured frame and tick rates over the last second, and App.droppedTicks counts ticks dropped after very slow frames. Watch them while you add content.
Next
- Running without a window
- Reference: CacheTreeMode, SpriteBatch