Skip to content

Add incremental rendering API to OfflineAudioContext#630

Open
cmzy wants to merge 1 commit into
orottier:mainfrom
cmzy:feature/offline-incremental-rendering
Open

Add incremental rendering API to OfflineAudioContext#630
cmzy wants to merge 1 commit into
orottier:mainfrom
cmzy:feature/offline-incremental-rendering

Conversation

@cmzy

@cmzy cmzy commented Jul 16, 2026

Copy link
Copy Markdown

OfflineAudioContext currently offers two rendering entry points, and
neither fits hosts that embed a JavaScript engine:

  • start_rendering_sync() renders the whole buffer in one call and
    consumes the renderer;
  • suspend()/suspend_sync() must be registered before rendering starts -
    suspending after the renderer has been taken panics with "cannot
    suspend when rendering has already started".

The Web Audio suspend/resume contract is inherently incremental: render
up to the suspend point, resolve the suspend promise, let script mutate
the graph inside the promise callback, resume, render the next segment.
An embedder cannot know the suspend points up front (script may schedule
new ones from inside a suspend callback), so a pull-style API is needed.

This adds three methods, mutually exclusive with suspend/suspend_sync:

  • render_upto_sync(frame): renders forward to the given frame (rounded
    up to whole render quanta), returns the total rendered frame count,
    and can be called repeatedly to continue;
  • rendered_frames_sync(): lock-free progress accessor (drives
    currentTime on the embedder side);
  • take_rendered_sync(): takes the finished AudioBuffer exactly once.

Implementation notes:

  • The rendered frame count is a lock-free AtomicUsize rather than being
    derived from the incremental state. The early-return paths of
    render_upto_sync report the count while holding the state mutex, and a
    std Mutex is not re-entrant: deriving the count through the same mutex
    self-deadlocks the calling thread on completely legal call sequences
    (render to the end, take the result, call render_upto_sync again). The
    atomic also keeps reporting the full length after the result has been
    taken instead of dropping back to 0.
  • While parked between segments the context state is set to Suspended
    (scripts observing state from a suspend callback must read
    "suspended" per spec), Running while a segment renders, and Closed
    after the final segment.
  • After finalizing, the event loop is spun once more so that the
    complete/statechange events queued by the finalization itself get
    delivered, matching the tail of start_rendering_sync.
  • RenderThread grows render_offline_quanta() (renders n quanta without
    consuming self) and finish_offline_render() (destructor run + event
    drain), splitting the existing one-shot render_audiobuffer_sync into
    resumable pieces.

Tests: chunked rendering produces bit-identical output to a one-shot
render of the same graph, state transitions are observable, the taken
result is single-shot, and post-completion / post-start_rendering calls
are safe no-ops.

OfflineAudioContext currently offers two rendering entry points, and
neither fits hosts that embed a JavaScript engine:

- start_rendering_sync() renders the whole buffer in one call and
  consumes the renderer;
- suspend()/suspend_sync() must be registered before rendering starts -
  suspending after the renderer has been taken panics with "cannot
  suspend when rendering has already started".

The Web Audio suspend/resume contract is inherently incremental: render
up to the suspend point, resolve the suspend promise, let script mutate
the graph inside the promise callback, resume, render the next segment.
An embedder cannot know the suspend points up front (script may schedule
new ones from inside a suspend callback), so a pull-style API is needed.

This adds three methods, mutually exclusive with suspend/suspend_sync:

- render_upto_sync(frame): renders forward to the given frame (rounded
  up to whole render quanta), returns the total rendered frame count,
  and can be called repeatedly to continue;
- rendered_frames_sync(): lock-free progress accessor (drives
  currentTime on the embedder side);
- take_rendered_sync(): takes the finished AudioBuffer exactly once.

Implementation notes:

- The rendered frame count is a lock-free AtomicUsize rather than being
  derived from the incremental state. The early-return paths of
  render_upto_sync report the count while holding the state mutex, and a
  std Mutex is not re-entrant: deriving the count through the same mutex
  self-deadlocks the calling thread on completely legal call sequences
  (render to the end, take the result, call render_upto_sync again). The
  atomic also keeps reporting the full length after the result has been
  taken instead of dropping back to 0.
- While parked between segments the context state is set to Suspended
  (scripts observing `state` from a suspend callback must read
  "suspended" per spec), Running while a segment renders, and Closed
  after the final segment.
- After finalizing, the event loop is spun once more so that the
  complete/statechange events queued by the finalization itself get
  delivered, matching the tail of start_rendering_sync.
- RenderThread grows render_offline_quanta() (renders n quanta without
  consuming self) and finish_offline_render() (destructor run + event
  drain), splitting the existing one-shot render_audiobuffer_sync into
  resumable pieces.

Tests: chunked rendering produces bit-identical output to a one-shot
render of the same graph, state transitions are observable, the taken
result is single-shot, and post-completion / post-start_rendering calls
are safe no-ops.
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.

1 participant