Skip to content

Fix mix transition reserving 9.2 MB per instance - #1332

Merged
ddennedy merged 2 commits into
mltframework:masterfrom
unicorntearsproject:fix-mix-dynamic-buffers
Oct 18, 2026
Merged

ddennedy merged 2 commits into
mltframework:masterfrom
unicorntearsproject:fix-mix-dynamic-buffers

Conversation

@unicorntearsproject

Copy link
Copy Markdown
Contributor

Fixes #1316, following the direction in the discussion there (dynamic allocation, using mlt_pool).

transition_mix embedded two MAX_SAMPLES * MAX_CHANNELS float buffers in its struct, so every mix was a ~9.2 MB calloc(). This allocates src_buffer and dest_buffer with mlt_pool_alloc() on demand instead. They only grow (at least doubling, up to the old maximum), the new space is zeroed and the old contents are kept, so reads see what they saw in the zero-initialised arrays. After they reach the size the frames need there is no allocation in the audio path. They are released in transition_close(). MAX_CHANNELS is unchanged.

Measured (RelWithDebInfo, 500 mix transitions created per round, repro), time / RSS:

round master this PR
1 4 ms / 56 MB 3 ms / 54 MB
2 29 ms / 120 MB 2 ms / 54 MB
3 1,504 ms / 4,448 MB 2 ms / 54 MB
4 346 ms / 4,448 MB 2 ms / 54 MB

Output is unchanged: 14 melt renders of different mix configurations (default, sum, start/end, linear and constant-power crossfade, reverse, combine, ducking, mixed sample rates and channel counts, chained mixes) are byte-identical to master. So is a harness driving transition_get_audio() with random frames (changing channel counts and frame sizes, discontinuities, paused flushes, and the buffer-overflow paths), 28,848 per-frame hashes, also clean under ASan/UBSan.

clang-format reports nothing on the changed lines (it reports 3 warnings on lines already in master, with a newer clang-format than CI's).

A note, not fixed here: *samples = MIN(src_count + samples_b0, dest_count + samples_a0) is computed before samples_a/samples_b are clamped, so a single frame above ~1.15M floats on both sides would read past count. It is unreachable with real frames, and master has the same issue.

🤖 Generated with Claude Code

transition_mix embedded two float[MAX_SAMPLES * MAX_CHANNELS] buffers in its
struct, so every mix transition was a 9.2 MB calloc(). Once glibc's dynamic
mmap threshold rises, each one comes from the heap and is zeroed in full: a
project with a few thousand audio crossfades needs tens of GB and seconds to
build its graph.

Allocate src_buffer and dest_buffer with mlt_pool_alloc() on demand instead.
They only ever grow, by at least doubling up to the old maximum, so there is
no allocation in the audio path once they have reached the size the frames
need. New space is zeroed and the old contents are kept, so reads see
exactly what they saw in the zero-initialised arrays, and the existing
overflow handling (which addresses the end of the full-size array) is
unchanged: it simply makes the buffer grow to the full size first.

Measured with 500 mix transitions created per round (4 rounds), RelWithDebInfo:
before, round 3: 1504 ms and 4448 MB RSS; after, every round 2-3 ms and
54 MB RSS (the baseline process size). Output is unchanged: melt renders of
14 mix configurations (default, sum, start/end, linear and constant-power
crossfade, reverse, combine, ducking, mixed sample rates and channel counts)
and a harness driving transition_get_audio with random frames (changing
channel counts, frame sizes, discontinuities, paused flushes and the buffer
overflow paths) are byte-identical to master, including under ASan/UBSan.

Fixes mltframework#1316.
@ddennedy ddennedy added this to the v7.44.0 milestone Oct 1, 2026
Comment thread src/modules/core/transition_mix.c Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Address the potential peak-memory regression from power-of-two pooled allocations and retained pool blocks.

Review effort: Lite
Findings: None

What changed in this PR

This PR reduces mix transition memory usage by replacing embedded audio buffers with lazy dynamic allocation.

Changes:

  • Adds growable, zero-initialized mlt_pool buffers.
  • Preserves existing audio data and limits.
  • Releases buffers during transition cleanup.
  • Requires addressing pooled allocation overhead for maximum-size buffers.
File Summary
src/​modules/​core/​transition_mix.c Implements dynamic audio buffer allocation and cleanup.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

As suggested in review. CLAMP(old * 2, floats, MAX_FLOATS) is the same
size as before: callers never ask for more than MAX_FLOATS.
@unicorntearsproject

Copy link
Copy Markdown
Contributor Author

Switched to CLAMP: CLAMP(old * 2, floats, MAX_FLOATS), which gives the same size as before since callers never ask for more than MAX_FLOATS. Re-checked: 7 mix configurations render byte-identical to before, and the 500-transition test stays at 2–3 ms and 54 MB per round. On Copilot's note: pooled blocks round up to a power of two, so the largest buffer (only reached in the overflow branch) is 8 MB instead of 4.6 MB; ordinary frames use a few tens of KB.

@ddennedy
ddennedy merged commit f0cafd2 into mltframework:master Oct 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

mix transition allocates 9.2 MB per instance

3 participants