OverviewFilesCommitsIssuesDocsReleasesPeers

ifsrenderer-linux commits / 993218e

The volume bake held four copies of itself; now it holds one
Measured on light_rods across resolutions, peak RSS was 2.4x the grid size — 4.27 GB at N=384 for a 906 MB grid. Not the grid: the chains around it. Each of `astype(float32) / HIST_SCALE`, `divide -> power -> transpose -> ascontiguousarray` and `divide -> clip -> transpose -> astype` materialises every intermediate at full size, and they overlap. Three changes, all output-preserving: * accumulate: `np.divide(raw, HIST_SCALE, dtype=np.float32)` converts and scales in ONE pass instead of allocating the float copy and then the quotient. * density: transpose to a single contiguous array first, then normalise and gamma IN PLACE, instead of a new array per step. * colour: built per z-slab, bounding the temporaries to one plane rather than four simultaneous copies of a 3-channel volume. N=384 peak 4.27 -> 3.38 GB (-21%) time 20.0 -> 13.8s (-31%) N=256 peak 1.62 -> 1.35 GB time 5.2 -> 4.0s Occupancy, VDB size and emission scale are identical at every resolution, and the geometry suites agree: volume_axes 5/5, auto_bounds 3/3, bake_orientation 7/7 (centroid within 0.03 voxels of analytic truth), bake_follows_document 3/3. Those are the right gates — the slab loop reindexes [z][y][x] to [x][y][z] by hand, and bake_orientation is exactly what catches getting that wrong. Also measured, and recorded here rather than acted on: occupancy FALLS as 1/N (43% at 128 to 7.3% at 384), because a dimension-2 attractor fills N^2 voxels of N^3. The dense grid is increasingly the wrong structure — VDB is already sparse on disk (11.5 -> 70.5 MB for 27x the voxels) but we pay dense everywhere else. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
993218ea29d82a6c6926585979c15b6ac9405876
by plankatron <fleeky@gmail.com> · 2026-08-18T15:42:18.000Z

Changed files (1) +27 -9

blender/ifs_volume.py +27 -9
……@@ -123,7 +123,15 @@ def accumulate(shader, doc, ubos, alias_tex, size, bounds, dispatches,
123123
124124 raw = np.array(volume.read(), dtype=np.uint32)
125125 # (z, y, 4x) as read back; split the interleaved channels out.
126 grid = raw.reshape(size, size, size, 4).astype(np.float32) / HIST_SCALE
126 #
127 # ONE allocation, not two. `raw.astype(np.float32) / HIST_SCALE` materialises
128 # the float copy AND the quotient — at N=384 that is two 906 MB arrays where
129 # one will do, and measured peak RSS was 2.4x the grid size because of chains
130 # like this. np.divide with an explicit dtype converts and scales in a single
131 # pass.
132 grid = np.divide(raw.reshape(size, size, size, 4), HIST_SCALE,
133 dtype=np.float32)
134 del raw
127135 return grid
128136
129137
……@@ -353,11 +361,13 @@ def write_vdb(grid: np.ndarray, bounds, path: str, name: str = "density",
353361 peak = float(density.max())
354362 if peak <= 0:
355363 raise RuntimeError("nothing was accumulated - the bounds may exclude the fractal")
356 normalised = density / peak
364 # Normalise IN PLACE on a single transposed copy rather than building one
365 # array per step. The old chain (divide -> power -> transpose -> contiguous)
366 # held up to four full-size temporaries at once; at N=384 each is 226 MB.
367 density = np.ascontiguousarray(density.transpose(2, 1, 0), dtype=np.float32)
368 density /= peak
357369 if gamma and gamma != 1.0:
358 normalised = np.power(normalised, 1.0 / gamma)
359 density = np.ascontiguousarray(
360 normalised.transpose(2, 1, 0), dtype=np.float32)
370 np.power(density, 1.0 / gamma, out=density)
361371
362372 grids = []
363373 dgrid = openvdb.FloatGrid()
……@@ -367,13 +377,21 @@ def write_vdb(grid: np.ndarray, bounds, path: str, name: str = "density",
367377 grids.append(dgrid)
368378
369379 # Colour is accumulated weighted by density, so divide it back out.
370 safe = np.maximum(grid[:, :, :, 3:4], 1e-9)
371380 # Same transpose as density - the spatial axes only, leaving the component axis
372381 # last. A colour grid transposed differently from density would tint the wrong
373382 # parts of the fractal, which is harder to spot than a whole-volume rotation.
374 colour = np.ascontiguousarray(
375 np.clip(grid[:, :, :, :3] / safe, 0.0, 1.0)
376 .transpose(2, 1, 0, 3).astype(np.float32))
383 #
384 # Built per SLAB. The whole-array form (divide -> clip -> transpose -> astype)
385 # peaked at four simultaneous copies of a 3-channel volume; slabs bound that to
386 # one plane at a time while producing byte-identical output.
387 colour = np.empty((size, size, size, 3), dtype=np.float32)
388 for z in range(size):
389 plane = grid[z] # (y, x, 4)
390 safe = np.maximum(plane[:, :, 3:4], 1e-9)
391 tinted = np.clip(plane[:, :, :3] / safe, 0.0, 1.0)
392 # grid is [z][y][x]; the target is [x][y][z], so this plane becomes a
393 # column across the first axis.
394 colour[:, :, z, :] = tinted.transpose(1, 0, 2)
377395 try:
378396 cgrid = openvdb.Vec3SGrid()
379397 cgrid.copyFromArray(colour)