Skip to content

HammerForge Performance and Limits

Last updated: October 23, 2026

How HammerForge's own work grows with the size of a level, measured rather than guessed, and the caps the code puts on single operations. Every time and size on this page comes from one run of tools/benchmark_level_scale.gd on the machine named below, on 22 October 2026, except Bake by layout and Round brushes, which come from a second run on 23 October; the few figures worked out from them say how. Run it on your own machine: the numbers move with the hardware, and the shape is what carries over.

The short version

  • Nothing caps the number of brushes in a level. The Status board's Geometry budget and the dock's health label are advice. They turn amber above 100 brushes and entities, and red above 1,000, where placing or undoing a brush, with the check after it, takes a little more than a frame.
  • What grows with brush count is the pause after placing or undoing. Resizing one brush took 0.36 ms at 100 brushes and 0.61 ms at 3,000, and the check after it 1.65 ms at 3,000: it looks at the brushes the step touched, where it used to walk every brush, 296 ms at 3,000 (#1619). Placing a brush and undoing one record the whole level: 26.0 ms and 45.0 ms at 3,000, with 28.5 ms and 19.1 ms for the checks after them, where on 23 October they were 163 ms and 199 ms with 212 ms for the check after the undo (#1866).
  • Picking stays cheap. A hover pick took 2.58 ms a mouse move at 3,000 brushes.
  • A full bake took 237 ms at 1,000 brushes and 848 ms at 3,000, headless. In the editor the default bake also waits for one editor frame every 8 brushes: 376 frames at 3,000.
  • Chunking and Bake Changed did not shorten the bake consistently. Both took about as long as a plain full bake and waited through the same frames.
  • Save a big scene as .scn. At 3,000 brushes the .scn was 928 KB against 10,440 KB of .tscn, and opened in 953 ms against 2,543 ms. It keeps its link to the .hflevel, and Save Scene As carries the level file's record across.
  • An agent's calls over the bridge grow slowly with the level: 3 ms a call at 111 brushes and 48 ms at 1,600. A placement records only what it made, near 2 ms at any size; the cuts, stairs and wires still record the whole level (#1786).
  • Save Level and autosave stop the editor while they take the level: 179 ms at 1,000 brushes and 566 ms at 3,000, every five minutes by default.

How it was measured

Machine AMD Ryzen 9 5900X, 12 cores and 24 threads; 31.9 GiB of memory as Godot reports it
OS Windows 11 Home, version 10.0.26300
Godot 4.7-stable (official), 5b4e0cb0f, run headless
HammerForge main at a6348abf, with the change that adds this page, which also stops a bake rereading two settings once for every brush
Date 22 October 2026
Run time 364.8 s for the whole run
godot --headless -s res://tools/benchmark_level_scale.gd --path . -- --counts=100,300,1000,3000 --repeats=5
  • The level is N box brushes, 2 m on a side, on a 4 m grid. No materials, no entities, no cuts, and the default bake settings. A real level with textures, cuts and entities costs more than this; read these figures as the floor.
  • Each figure is the median of 5 runs. Within a run a pick is the mean of 50 pointer positions, and an edit the mean of 10.
  • Headless, so nothing is drawn. Every time is HammerForge's work and Godot's under it, with no rendering. Where the editor adds frames of its own, the tables count the frames rather than time them, because a frame's length depends on the editor's frame rate and on what is on screen.
  • The machine was a desktop in use, with other Godot processes running beside the benchmark, so read small differences between two cells with care.
  • Sizes are in KB of 1,024 bytes.

Without arguments the script measures 100, 300 and 1,000 brushes with 3 runs each. --counts= takes a comma list of brush counts, --repeats= the runs per figure, and --only= the parts to measure (picks, edits, bakes, files, round). It writes only into a folder of its own under user://, which it removes when it ends, and never touches the editor's preferences.

Picking

Brushes 100 300 1,000 3,000
Pointer positions over a brush, of 50 24 30 28 32
Hover pick, one mouse move (ms) 0.09 0.33 0.50 2.58
Click pick (ms) 0.18 0.46 0.93 4.42

The Select tool picks on every mouse move with no button held, and a click picks once. Both test each brush's world box before the exact test (#1387), so they stay well inside a frame. The camera sat above one corner of the grid and the pointer swept across the view; the first row says how many of the 50 positions were over a brush.

Editing

Measured again on 26 October 2026 with --only=edits --repeats=3, on the same machine, after an undo stopped walking every node for things nothing could have changed (#1866).

Brushes 100 300 1,000 3,000
Resize one brush, with its undo step (ms) 0.36 0.37 0.41 0.61
Place one brush, with its undo step (ms) 0.98 2.26 7.97 26.0
Undo that placement (ms) 1.42 3.73 12.9 45.0
Change check after the resize (ms) 1.20 1.24 1.36 1.65
Change check after the placement (ms) 1.88 3.47 9.19 28.5
Change check after its undo (ms) 1.59 2.61 6.47 19.1

Before #1866 the same run measured 173 ms to place a brush at 3,000, 233 ms to undo it and 228 ms for the check after the undo (25 October). Keeping each brush's faces record between steps brought the undo to 73.8 ms, and walking less brought it to 45.0 ms.

  • Resize one brush is a handle drag as it commits: one undo step that records only that brush.
  • Place one brush is drawing a box. Its undo step records the whole level, because a new brush changes which brushes exist, and Undo puts the whole level back. Deleting records the whole level in the same way. Each brush keeps the record of its faces until one can have changed, and every step taken meanwhile shares it, so a step costs time for the brushes that changed. An undo keeps a brush whose record shares those faces without comparing it face by face. What is left of an undo at 3,000 is building the rest of each kept brush's record to compare it, about 25 ms, and putting every kept brush back in the registries, about 6 ms. The visgroups are drawn again for the brushes and entities the undo built, and for every node only when the visgroups, the view or its filters changed. The walks that take stale structure, array and prefab tags off are skipped when the level has none before or after, and the brushes are reordered only when they are out of order.
  • Change check is the pass the plugin runs after every step that lands in the undo history, Undo and Redo included (HFBrushChangeTracker.reconcile_step()), so that a change Godot's own gizmo, Inspector or undo made reaches Bake Changed and the level's records. It compares the brushes the step can have touched with what they were: the selection, what HammerForge's own commands tagged, and what an undone or redone step changed. A brush a command made is taken in without the rest. When it cannot tell, as after an undo that rebuilt every brush, it compares every brush, and a bake compares every brush first when a check since the last full one looked at only some. It compared every brush after every step until #1619: 8.21 ms at 100 brushes and 296 ms at 3,000.

The pause after an edit is the edit and the check together. Adding the two rows:

Brushes 100 300 1,000 3,000
Resize, then the check (ms) 1.56 1.61 1.77 2.26
Place, then the check (ms) 2.86 5.73 17.2 54.5
Undo a placement, then the check (ms) 3.01 6.34 19.4 64.1

A resize and its check fit in one frame at 60 frames a second, 16.7 ms, at every size measured. Placing a brush and undoing it fit at 100 and 300 brushes, and take a little more than one frame at 1,000. Those are the thresholds the Status board's Geometry budget, the dock's health label and its brush count use: amber above 100 brushes and entities, red above 1,000. They were 50 and 100, which turned the count red at a size where nothing measured here is slow.

A session over the bridge

Measured on 23 October 2026 on the same machine with tools/benchmark_bridge_session.gd (#1652), from one run with other Godot processes running beside it.

godot --headless -s res://tools/benchmark_bridge_session.gd --path . -- --scale=1

It drives the bridge's dispatcher directly, with no socket, and builds a town the way an agent would: per room a floor, four walls, a doorway cut, a flight of stairs, a lamp, a trigger tied to trigger_once and a door tied to func_door, wired, the floor painted, and a count read back. That is 15 calls a room, and --scale=1 is 100 rooms, 1,500 calls. Each figure is the mean per call over the 100 calls up to that count.

Calls 100 300 500 700 1,000 1,500
Brushes 111 320 533 751 1,071 1,600
Whole call (ms) 2.7 8.9 13.9 20.8 30.0 47.6
Slowest call (ms) 21.0 44.5 67.2 94.6 135.0 203.5
Undo record (ms) 1.3 6.6 10.8 16.6 24.2 39.0
Undo record of one solid placement (ms) 0.17 0.47 0.77 1.08 1.57 2.48
The change and its answer (ms) 1.0 1.0 1.0 1.1 1.2 1.4
Change check after it (ms) 2.0 5.8 9.0 13.3 19.0 29.4
  • A placement records only what it made, so its undo record stays near 2 ms at every size: 2.48 ms at 1,600 brushes. Before #1786 it recorded the whole level, and a call took 165 ms at 1,600 brushes against 48 ms now.
  • What still grows is the undo record of the rest: a room's cut, its stairs and its wire record the whole level, as deleting does, and those three make up most of the 39 ms.
  • The change itself stays near 2 ms at every size.
  • The change check is the plugin's pass after every step, as under Editing.
  • The slowest call in a block is one call, and moves the most from run to run: 203 ms in the block up to 1,500 calls, against a mean of 48 ms.
  • All 1,500 calls were answered. At the end, saving the level took 1,162 ms until the file was written, and opening it again 1,684 ms.

--scale= takes a fraction or a multiple of the 100 rooms. The script saves into user:// and removes the file when it ends.

Baking

Brushes 100 300 1,000 3,000
Full bake (ms) 19.8 80.2 237 848
Full bake, chunked at the recommended size (ms) 25.9 80.5 191 808
Bake Changed, one brush moved (ms) 19.4 61.9 207 832
CSG bake, the path a level with a cut takes (ms) 13.5 59.8 122 593

Bake shape

Brushes 100 300 1,000 3,000
Full bake, frames it waits through 14 39 126 376
Full bake, chunked at the recommended size, frames it waits through 14 39 126 376
Recommended chunk size (m) 9.50 17.5 31.5 54.5
Chunks that bake makes 16 16 16 16
Bake Changed, one brush moved, frames it waits through 14 39 126 376
CSG bake, the path a level with a cut takes, frames it waits through 2 2 2 2
  • Full bake is timed from the call to its answer. That is longer than the Last bake the dock shows, which leaves out the frames the bake waits for and the passes after the mesh is built.
  • Frames it waits through. With Use Face Materials on and no cuts, the default, a bake hands a frame back to the editor every 8 brushes so the editor stays responsive. Headless such a frame is close to free. In the editor each one is a whole editor frame, so the wait there is the time above plus that many frames.
  • Chunking at the size the Status board recommends, a quarter of the level's longest side, made 16 chunks at every count. It took 191 ms against 237 ms at 1,000 brushes and 808 ms against 848 ms at 3,000, was slower at 100 (25.9 ms against 19.8 ms), and waited through the same frames. It splits the baked mesh into pieces the renderer can cull one at a time, which a big level wants in the game; it did not shorten the bake consistently.
  • Bake Changed after one brush moved took about as long as a full bake (207 ms against 237 ms at 1,000 brushes, 832 ms against 848 ms at 3,000) and waited through the same frames, because when anything has changed it rebuilds the whole level. What it saves is the bake when nothing has.
  • The CSG path, which a level takes once it holds a cut, was faster on this level: 593 ms at 3,000 brushes, and 2 frames. These boxes do not touch, which is the easiest case CSG has. A level of brushes that overlap and cut each other is not measured here.

The bake time estimate in the Test tab assumes 2 ms a brush until a bake has run. Measured here the bake took between 0.198 ms a brush (19.8 ms for 100) and 0.283 ms a brush (848 ms for 3,000), before the editor's frames. After a bake the estimate is the dock's Last bake divided by the brushes that bake took, times the brushes in the level now. A Bake Selected took only its selection, so one brush baked alone gives one brush's time, not a share of the level's (#1590).

Bake by layout

The boxes above stand apart, the easiest case the CSG boolean has. A second run on 23 October, on the same machine, packed the same boxes 2 m apart so each shares a face with its neighbours, and baked them both ways; then it gave the spaced boxes a 1 m cut into the top corner of every tenth (#1624). --only=layouts runs it.

Brushes 100 300 1,000 3,000
Touching boxes, face material bake (ms) 22.2 70.2 209 648
Touching boxes, CSG bake (ms) 29.7 86.8 285 899
Spaced boxes, a cut in every tenth, bake (ms) 29.3 136 1,004 9,324
  • Touching boxes turn the comparison round: the face material path, which Use Face Materials picks when there is no cut, was faster at every count, 648 ms against 899 ms at 3,000. On spaced boxes the CSG path was faster, 593 ms against 848 ms. The face material path still hands the editor a frame every 8 brushes (376 frames at 3,000) and CSG two in all, so in the editor the face path's wait is the time plus those frames.
  • A level with a cut bakes on the CSG path whatever Use Face Materials says, because the face path has no boolean step. One cut in ten boxes took 1.0 s at 1,000 brushes and 9.3 s at 3,000, ten times the touching boxes' CSG bake: the boolean's cost grows with the cuts as well as the brushes. A cut that cuts no solid brush still sends the bake this way, and Validate finds one.
  • Not measured: brushes that overlap, and cuts through touching walls.

Round brushes

Since #1613 every round brush goes into the CSG bake as its own faces, built in GDScript, which matches its shape exactly. That cost about 1.8 ms per 128-face sphere at every bake. The operand is kept now, by brush, and reused while neither the brush's faces nor the materials they resolve to change (#1628). The same run, before and after, with --only=round:

Brushes 100 300
CSG bake, 16-sided spheres set flat, operand built each bake (ms) 365 1,109
CSG bake, the same, operand kept (ms) 208 744
CSG bake, 16-sided spheres smoothed, operand built each bake (ms) 789 2,429
CSG bake, the same, operand kept (ms) 632 2,421

Each figure is the median of three bakes, so it is the bake after the first. The first bake of a session builds every operand. The flat spheres now bake in the time Godot's own primitives took, 205 ms for 100. The smoothed ones gain less: putting the shading back after the boolean (#1612) is most of their time.

Files

Sizes

Brushes 100 300 1,000 3,000
.hflevel, compressed (KB) 4.45 11.1 32.7 94.8
.hflevel, plain (KB) 478 1,426 4,746 14,240
.tscn, Brushes and bake (KB) 338 1,012 3,373 10,440
.scn, Brushes and bake (KB) 34.9 98.2 317 928
.tscn, Brushes only (KB) 200 595 1,976 5,934
.scn, Brushes only (KB) 15.1 40.8 130 386
.tscn, Baked geometry only (KB) 141 420 1,400 4,509
.scn, Baked geometry only (KB) 21.1 58.5 188 544

Saving

Brushes 100 300 1,000 3,000
Save .hflevel, compressed, editor blocked (ms) 13.7 65.6 179 566
Save .hflevel, compressed, until written (ms) 86.2 294 970 2,898
Save .hflevel, plain, editor blocked (ms) 23.7 59.7 208 556
Save .hflevel, plain, until written (ms) 99.2 325 1,018 3,298
Save .tscn, Brushes and bake (ms) 57.4 131 536 1,601
Save .scn, Brushes and bake (ms) 47.1 87.7 381 1,312
Save .tscn, Brushes only (ms) 42.1 95.8 358 1,388
Save .scn, Brushes only (ms) 46.9 112 254 1,213
Save .tscn, Baked geometry only (ms) 9.36 19.5 34.9 176
Save .scn, Baked geometry only (ms) 3.66 5.00 12.1 51.5

Opening

Brushes 100 300 1,000 3,000
Open .tscn, Brushes and bake (ms) 75.0 231 739 2,543
Open .scn, Brushes and bake (ms) 34.1 101 254 953
Open .tscn, Brushes only (ms) 64.8 174 509 1,787
Open .scn, Brushes only (ms) 34.4 112 361 915
Open .tscn, Baked geometry only (ms) 116 329 1,309 3,721
Open .scn, Baked geometry only (ms) 93.4 299 1,194 2,752
Load .hflevel, compressed (ms) 118 305 1,193 3,215
Load .hflevel, plain (ms) 111 315 1,141 3,169

.scn or .tscn

A binary .scn was about a tenth the size of the same scene as .tscn (34.9 KB against 338 KB at 100 brushes, 928 KB against 10,440 KB at 3,000) and opened faster (953 ms against 2,543 ms at 3,000). The price is that a .scn is not text: a diff shows nothing, and two people's changes to it cannot be merged. Keep .tscn while the scene is reviewed and merged like code, and switch with Scene > Save Scene As... once the level is too big for that to matter. The benchmark saves a .scn compressed, as the editor does when its Compress Binary Resources setting (FileSystem > On Save) is on.

A level saved as .scn keeps both halves of its link to its .hflevel. The scene holds the .hflevel path, and a Save Level from a level opened out of a .scn records that .scn in the file, so the level is told about its own file and autosaves to it as it would from a .tscn (see Which File Opens). tests/test_scene_contents_choice.gd saves a level as a compressed .scn, opens it and checks both, and checks that a Baked geometry only .scn loads its brushes from the .hflevel it names.

Save Scene As carries the record. With autosave on, saving the scene under a new name writes the .hflevel at once, so it names the .scn and later autosaves go on as before. A level switched with autosave off, or in an older version, still names the .tscn: autosave refuses to write over it and says so once. Press Save Level once and the file names the .scn.

Scene Keeps

What the Scene Keeps decides what Ctrl+S writes.

  • Brushes and bake, the default, is the largest scene and needs no other file.
  • Brushes only leaves the bake out: 386 KB against 928 KB as .scn at 3,000 brushes. Bake again after opening it to have geometry.
  • Baked geometry only is the quickest to save (51.5 ms as .scn at 3,000) and the slowest to open, because the brushes are rebuilt from the .hflevel when it opens: 2,752 ms as .scn against 953 ms for Brushes and bake.

The .hflevel

Compressed, the default, the .hflevel was 94.8 KB at 3,000 brushes; plain it was 14,240 KB. Save Level stops the editor while it takes the level (566 ms at 3,000 brushes) and then writes on another thread, so the file was on disk after 2,898 ms. Autosave runs the same save every 5 minutes by default, set from 1 to 60. Loading a .hflevel into an empty level took 3,215 ms at 3,000 brushes.

A big level

  • Split the map into scenes. The check after every edit, the whole-level undo steps and Save Level all work over one level's brushes, so each level pays for its own brushes rather than for the whole map's.
  • Save the scene as .scn once nobody needs to merge it, then press Save Level.
  • Chunk the bake for the game, not for a shorter bake.
  • Expect Bake Changed to take as long as Bake whenever anything has changed.
  • Lengthen the autosave interval (Console > Controls, Autosave every (min)) if its pause gets in the way.

Limits the code enforces

These are caps in the code, not measurements. Past its value each one refuses, clamps or falls back to something simpler, as the table says, rather than slowing down. There is no cap on the brushes or entities in a level.

Limit Value Where
Brushes one array makes 256 MAX_COPY_BRUSHES, hf_duplicator.gd
Steps in a staircase or a spiral staircase 128 MAX_STEPS, hf_stairs_builder.gd and hf_spiral_stairs_builder.gd
Brushes in one flight of stairs, landings and nosings included 128 MAX_PIECES, hf_stairs_builder.gd
Panels in a dome (rings x segments) 256 MAX_PANELS, hf_dome_builder.gd
Segments in an arched door frame 2 to 64 MIN_SEGMENTS, MAX_SEGMENTS, hf_door_frame_builder.gd
Sides of a ring's tube 3 to 32 MIN_TUBE_SEGMENTS, MAX_TUBE_SEGMENTS, hf_ring_builder.gd
Segments of a round brush 64 MAX_ROUND_BRUSH_SEGMENTS, brush_instance.gd
Faces of a brush Hollow, Carve or Intersect will work on 128 MAX_BOOLEAN_PLANES, hf_convex_clip.gd
How deep one Extrude goes 256, past which a typed depth is refused MAX_EXTRUDE_HEIGHT, hf_extrude_tool.gd
Thinnest band a trim profile is cut into 1 cm, so a fine Trim Segments can give fewer strips MIN_BAND, hf_trim_profile.gd
Cuts tried when splitting a concave outline 256 MAX_CUTS, hf_polygon_split.gd
Material surfaces in one brush's editor preview 200, past which it draws its plain mesh MAX_PREVIEW_SURFACES, brush_instance.gd
Faces of a brush that snapping aims at 64, past which it snaps to the brush's box MAX_SNAP_FACES, hf_snap_system.gd
Cut overlays drawn at once 50 MAX_PREVIEWS, systems/hf_subtract_preview.gd
Cuts given a live CSG preview at once 8 MAX_CSG_SUBTRACTORS, systems/hf_subtract_preview.gd
Carve and intersect previews drawn at once 50 MAX_PREVIEWS, systems/hf_carve_preview.gd
Bake chunk size 0 (off), or 1 to 16,384 MIN_BAKE_CHUNK_SIZE, MAX_BAKE_CHUNK_SIZE, level_root.gd
Material atlas 4,096 px MAX_ATLAS_SIZE, hf_material_atlas.gd
Lightmap Scale of a brush or a visgroup 0.125 to 8 MIN_LIGHTMAP_SCALE, MAX_LIGHTMAP_SCALE, hf_bake_split.gd
Texture layers blended over one face 8 MAX_PAINT_LAYERS, surface_paint.gd; MAX_SURFACE_PAINT_LAYERS, level_root.gd
Cells one bucket fill covers 500,000 MAX_BUCKET_FILL_CELLS, paint/hf_paint_tool.gd
Paint cell index, either axis 100,000 from the origin CELL_LIMIT, systems/hf_paint_system.gd
Cells one rectangle paint call covers 65,536 MAX_RECT_CELLS, systems/hf_paint_system.gd
Paint region size, and streaming memory budget 64 to 4,096 cells; 32 to 8,192 MB systems/hf_paint_system.gd
Instances one scatter stroke places 50,000 MAX_SCATTER_INSTANCES, paint/hf_scatter_brush.gd
Autosave interval, and backups kept 1 to 60 minutes; 1 to 50 level_root.gd
Changes in one agent hf_batch 100 BATCH_LIMIT, mcp/hf_mcp_dispatcher.gd
Agent request, and response 1 MiB; 16 MiB MAX_REQUEST_BYTES, MAX_RESPONSE_BYTES, mcp/hf_mcp_protocol.gd

Paths are under addons/hammerforge/.