Release v0.3.0-rc1 #6

Merged
psk merged 14 commits from feature/moar into master 2026-06-27 20:45:48 +00:00
Owner

Cuts v0.3.0-rc1 (release candidate). Merging this triggers tagging → the pipeline builds + publishes all artifacts incl. the NVIDIA -nvenc binary, marked as a pre-release.

Added

  • Headless render-to-video (soundy-bits render …, native-only). The same binary renders a
    clip to an MP4 (H.264 video + Opus audio, in sync) or a PNG frame sequence with no window or
    display — deterministic and reproducible, runnable on a headless server. Reuses the real
    visualization/post-process plugins via an offscreen RenderTarget::Image + gpu_readback, a
    fixed-step clock, and a frame-indexed offline audio pump. Encoding is a pluggable
    VideoSink/VideoEncoder seam (src/headless/) so AV1 can drop in. Requires libopus-dev at
    build time and libopus.so at runtime.
    • Hardware H.264 via NVENC (--features nvenc, NVIDIA) — auto-selected by default with the
      software encoder as fallback; capped-VBR with a configurable --bitrate. The crate vendors its
      headers and dlopens the driver at runtime, so it builds with no CUDA toolkit (but hard-links the
      NVIDIA encode lib, so the nvenc binary only runs on NVIDIA hosts — hence an opt-in feature, not
      default). Reuses a pooled, pipelined set of NVENC buffers, so a full song renders at ~7× real
      time at 1080p60
      (≈420 fps on a 4090) and exits cleanly.
    • Software encoder (fallback) is multi-threaded: encode/mux on a worker thread overlapping GPU
      rendering, constant-QP + multi-slice so it parallelizes across cores, parallel RGBA→I420.
    • Flags: --encoder auto|hw|sw, --bitrate <Mbps> (NVENC), --quality <QP> (software CQP,
      default 22), --speed (faster software), --software (render on Mesa lavapipe for GPU-less hosts).

Changed

  • Upgraded to Bevy 0.19. The custom 3-pass post-process chain (Tilemap → Combined effects →
    Vector SDF/filters) and the ColoredMesh2d pipeline were ported from the removed render-graph
    node API to the new schedule-based renderer (passes are render-world systems on the Core2d
    schedule, ordered after tonemapping in Core2dSystems::PostProcess). UI scaling moved from
    EguiContextSettings::scale_factor to the EguiZoomFactor component, and egui/Bevy deprecations
    were cleared.
  • Refreshed all dependencies to their latest compatible versions. cpal is intentionally held at
    0.17 to stay aligned with rodio 0.22 (which pins cpal 0.17), avoiding a duplicate native audio
    backend in the tree; it will follow once rodio bumps.
Cuts **v0.3.0-rc1** (release candidate). Merging this triggers tagging → the pipeline builds + publishes all artifacts incl. the NVIDIA `-nvenc` binary, marked as a pre-release. ### Added - **Headless render-to-video** (`soundy-bits render …`, native-only). The same binary renders a clip to an MP4 (H.264 video + Opus audio, in sync) or a PNG frame sequence with no window or display — deterministic and reproducible, runnable on a headless server. Reuses the real visualization/post-process plugins via an offscreen `RenderTarget::Image` + `gpu_readback`, a fixed-step clock, and a frame-indexed offline audio pump. Encoding is a pluggable `VideoSink`/`VideoEncoder` seam (`src/headless/`) so AV1 can drop in. Requires `libopus-dev` at build time and `libopus.so` at runtime. - **Hardware H.264 via NVENC** (`--features nvenc`, NVIDIA) — auto-selected by default with the software encoder as fallback; capped-VBR with a configurable `--bitrate`. The crate vendors its headers and dlopens the driver at runtime, so it builds with no CUDA toolkit (but hard-links the NVIDIA encode lib, so the `nvenc` binary only runs on NVIDIA hosts — hence an opt-in feature, not default). Reuses a pooled, pipelined set of NVENC buffers, so a full song renders at **~7× real time at 1080p60** (≈420 fps on a 4090) and exits cleanly. - Software encoder (fallback) is multi-threaded: encode/mux on a worker thread overlapping GPU rendering, constant-QP + multi-slice so it parallelizes across cores, parallel RGBA→I420. - Flags: `--encoder auto|hw|sw`, `--bitrate <Mbps>` (NVENC), `--quality <QP>` (software CQP, default 22), `--speed` (faster software), `--software` (render on Mesa lavapipe for GPU-less hosts). ### Changed - **Upgraded to Bevy 0.19.** The custom 3-pass post-process chain (Tilemap → Combined effects → Vector SDF/filters) and the `ColoredMesh2d` pipeline were ported from the removed render-graph node API to the new schedule-based renderer (passes are render-world systems on the `Core2d` schedule, ordered after tonemapping in `Core2dSystems::PostProcess`). UI scaling moved from `EguiContextSettings::scale_factor` to the `EguiZoomFactor` component, and egui/Bevy deprecations were cleared. - Refreshed all dependencies to their latest compatible versions. `cpal` is intentionally held at 0.17 to stay aligned with `rodio` 0.22 (which pins `cpal` 0.17), avoiding a duplicate native audio backend in the tree; it will follow once rodio bumps.
psk added 14 commits 2026-06-27 20:31:21 +00:00
Retire the git-flow ritual in favour of trunk-based GitHub Flow: master is
the trunk and a release is a tag on master.

- justfile: collapse release/release-current/release-dev/hotfix/finish into a
  single `just release <part>` that bumps, cuts the CHANGELOG, tags, and pushes
  from master. Keep bump/bump-pre/re-release.
- release.yml: restore tag-only triggering (drop the TEMP feature/moar branch
  trigger, the `-test` version synthesis, and the ref_type guards).
- ci.yml: run check/clippy/test on any branch push + PRs into master.
- README: fix stale keybinds (R is Randomize not "mirror"; C is all modes;
  arrows seek/volume; add Y loopback + Del), add Cymatics to the mode/config
  lists, fix the placeholder clone URL, add a Download/Releases section and the
  live wasm demo links.
- CLAUDE.md / CONTRIBUTING.md: document the release process, harden the
  "tests MUST pass before master/release" gate (assets are embedded from disk
  and only validated at test/runtime), fix the four->five mode drift.
- keybinds.rs: drop the stale in-app "R = Mirror" row, add the Y loopback row.
- Cargo.toml: add description/repository/homepage/license metadata.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the stale refactor/streaming notes and the Forgejo runner + git-pages
ops runbooks. Replace with a roadmap for two native-only video features:
headless render-to-video (deterministic, offline, preset-driven) and live
framebuffer capture (Tsoding-style). Encoding uses Rust crates (openh264 or
rav1e → muxide/mp4e MP4, audio as Opus via audiopus), no runtime ffmpeg.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bevy 0.19 replaced the render-graph-node API with a schedule-based
renderer and changed several mesh2d/egui/asset APIs. The previous WIP
commit only bumped Cargo.toml and left the tree uncompilable.

Rendering
- Port the custom 3-pass post-process chain (Tilemap -> Combined
  effects -> Vector SDF/filters) from `impl ViewNode` + render-graph
  nodes/edges to render-world systems on the `Core2d` schedule, ordered
  `.after(tonemapping).in_set(Core2dSystems::PostProcess)`.
- Order the chain `.before(bevy_egui::render::egui_pass)` so egui
  composites on top of the finished frame instead of being fed through
  the effects (0.19 replacement for the old egui edge-removal hack).
- Port `ColoredMesh2d` to 0.19: `Mesh2dPipelineKey::from_target_format`
  / `from_primitive_topology_and_strip_index`, `RenderVisibleEntities::
  get`/`iter_visible`, `add_transient`, `Option` depth fields,
  `SyncComponent` impl, `Affine3` conversion, `Rgba8UnormSrgb`.
- Guard zero-vertex meshes (`non_empty_mesh`): 0.19's mesh allocator
  logs a use-after-free for empty meshes (skips allocation but still
  copies), which our per-frame rebuilds hit whenever there is no audio.

Other API changes
- `Assets::get_mut` now returns `AssetMut` (mut bindings).
- egui UI scale: `EguiContextSettings::scale_factor` -> `EguiZoomFactor`.
- Clear egui/Bevy deprecations (insert_non_send, egui_wants_keyboard_
  input, is_pointer_over_egui, global_style, Panel::right/*_size).
- Migrate examples/simple_viz_shader.rs the same way.

Deps / docs
- Refresh all deps to latest compatible versions; hold cpal at 0.17 to
  stay aligned with rodio 0.22 (avoids a duplicate audio backend).
- Update stale "Bevy 0.18" references and add an Unreleased changelog.

check + clippy clean (zero warnings), all tests pass, app verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`soundy-bits render --input <audio> --out <dir> --width W --height H
--fps F [--preset f] [--from t --to t] [--codec h264|av1] [--gain G]`
renders a clip offline with no window or display and writes one PNG per
frame. Verified end-to-end on a headless GPU (Vulkan): a 2s/30fps orbit
preset clip produced 60 correct, audio-reactive, post-processed frames.

How it works:
- main() branches into headless::run before building the windowed app.
- Windowless app: DefaultPlugins with the primary Window *entity* sized
  WxH (so the visualization's many Query<&Window> reads keep working)
  but WinitPlugin disabled (no surface/event-loop) + ScheduleRunnerPlugin.
- Only the render plugins are registered (ColoredMesh2d, PostProcess,
  Visualization, Effects) plus ConfigResource/AudioSeedResource; UI,
  input, audio-device and window/system-settings plugins are dropped.
- Camera retargeted to an offscreen RenderTarget::Image (RGBA8) which the
  3-pass post-process chain renders into; gpu_readback::Readback +
  ReadbackComplete observer route frames to a pluggable VideoSink.
- Deterministic clock via TimeUpdateStrategy::ManualDuration(1/fps); an
  offline decode (rodio_audio::decode_file_to_samples) feeds the analyzer
  a frame-indexed window each tick and populates AudioData through the
  shared populate_audio_data helper (extracted from process_audio_system
  so live and headless can't drift).
- A preset overlays the default AppConfig before setup; --gain scales the
  analysed signal. WARMUP_FRAMES discarded so recorded frames have the
  full effect stack; exactly ceil(dur*fps) frames recorded then AppExit.

Encoder backends (H.264/AV1) plug in behind VideoSink next; the PNG sink
stays as a fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`--out foo.mp4` now writes a real, playable H.264 MP4 instead of a PNG
directory (PNG sink kept as a fallback for any other --out path).
Verified headless: a 3s/720p/30fps render produced a standards-compliant
MP4 (ftyp/moov/mdat fast-start) with exactly 90/90 frames per ffprobe.

Modular encoder seam so AV1 can drop in later without touching the
render/readback/mux path:
- VideoEncoder trait + new_encoder(codec, w, h, fps) factory
  (src/headless/encode/). --codec h264 -> OpenH264Encoder; --codec av1
  returns a clear "not yet implemented" pointing at the seam.
- OpenH264Encoder (openh264, vendored C via cc): quality-scaled bitrate,
  RGBA8 -> I420, skip_frames(false) so output frame count == input and
  A/V will stay in sync.
- Mp4Sink (muxide, pure-Rust MP4 muxer) owns encoder + muxer and writes
  Annex-B frames with their pts/keyframe flag; finish() finalizes moov.
- Dimensions rounded down to even (I420 requirement).

Audio (Opus) track muxes in next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`--out foo.mp4` now produces a full A/V file. Verified: a 4s render gave
H.264 video + Opus 48 kHz stereo audio, both streams exactly 4.000s
(in sync) per ffprobe (120 video frames + 200 20ms audio packets).

- encode/opus.rs: resample the source clip [from,to] to Opus's 48 kHz
  (linear interpolation; ample for near-unity 44.1->48k upsampling) and
  encode 20ms frames via audiopus (links system libopus; needs
  libopus-dev). Each packet carries a pts on the video timeline.
- The audio track is encoded from the *ungained* source, so the muxed
  music plays at normal volume while --gain only scales viz reactivity.
- Mp4Sink configures a muxide Opus track and writes all audio packets at
  finish() (muxide buffers for fast-start, so batched audio is valid).
- PngSink path skips audio entirely.

Phase 1 (headless render-to-video) is feature-complete: deterministic
offline render -> H.264/Opus MP4, fully headless, AV1 pluggable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- CLAUDE.md: video-output deps, a "Headless render-to-video" section
  (render subcommand, how the windowless path works, the libopus build
  requirement), and src/headless/ in the project tree.
- README.md: libopus-dev in the Linux prerequisites, and a "Render to
  Video (Headless)" usage section.
- CONTRIBUTING.md: note that native builds need libopus-dev.
- CHANGELOG.md: Unreleased "Added" entry for the headless renderer.
- TODO.md: mark Mode 1 implemented; correct the audiopus note (links
  system libopus, not vendored); add a "Headless in releases" plan
  covering the CI/runner blocker (no libopus on the sudo-less runner)
  with three routes (provision libopus / feature-gate / static libopus)
  and a recommendation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The aarch64 release build runs via `cross` in an isolated container, so
the libopus installed on the host runner isn't visible to it — audiopus
failed to link there. Add libopus-dev:$CROSS_DEB_ARCH to the existing
pre-build apt list (same mechanism as the bevy/cpal target libs).
OpenH264 builds its C with cross's bundled target toolchain, so it needs
no extra packages.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Renders a full song to MP4 on the GPU's NVENC encoder at ~420 fps @1080p60
(≈7x real-time) on a 4090, falling back to a multi-threaded software
encoder when hardware isn't available. The earlier single-threaded
software encode ran inside the render loop, so a 1080p60 song never
finished and left a 0-byte file; this is fast, hardware-accelerated, and
exits cleanly.

Encoder selection (--encoder auto|hw|sw, default auto = hardware-first,
logs the choice and warns on a hardware->software fallback):
- NVENC (encode/nvenc.rs, `nvenc` feature): hardware H.264 on NVIDIA.
  Readback RGBA8 -> NV_ENC_BUFFER_FORMAT_ABGR (no CPU YUV), low-latency
  tuning (no B-frames), capped VBR via --bitrate. Reuses a POOLED and
  PIPELINED set of input/output buffers: allocating buffers per frame
  cost ~4.8 ms and draining each synchronously another ~2.3 ms — both now
  hidden, taking throughput 106 -> 420 fps. The Buffer/Bitstream borrow
  the Session, so it is boxed and the pool's borrows are lifetime-extended
  (dropped before the session). The !Send Session is used under a
  documented single-worker-thread contract (bind_to_thread on first
  encode).
- OpenH264 (software fallback): constant-QP (--quality), multi-slice so
  num_threads parallelizes, parallel RGBA->I420 (encode/yuv.rs).

Build/publish:
- The crate vendors its headers + dlopens libcuda (cudarc pinned to
  cuda-12020), so it needs no CUDA toolkit. It hard-links
  libnvidia-encode, so it stays an opt-in feature (the binary only runs
  on NVIDIA) and `auto` falls back to software if NVENC can't init.
- scripts/nvenc-stub-libs.sh lets a driver-less, sudo-less CI runner build
  the artifact: the binary imports just 2 symbols, so stub libs with the
  real SONAMEs satisfy the linker while the real driver loads at runtime.

Pipeline:
- VideoEncoder::encode returns 0+ frames + a finish() flush (hardware
  encoders pipeline); the sink pairs each output with the oldest pending
  pts (no reordering). ThreadedSink runs encode+mux on a worker thread,
  built after the app so the encoder choice is logged.
- Software render via WgpuSettings; --software forces Mesa lavapipe.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`just release` already bumps + commits the version; `just unstable` cut a
prerelease tag off the current Cargo.toml without bumping it, so the
built binary reported the wrong version and the release pipeline's
tag-vs-Cargo.toml sanity check would fail. Now it bumps Cargo.toml to the
prerelease version, commits it, and tags that commit (clean tree
required), matching `just release`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The release pipeline now produces a second x86_64 Linux artifact built
with --features nvenc, alongside the software one. The driver-less,
sudo-less runner can build it because the binary imports just two symbols
from libnvidia-encode: scripts/nvenc-stub-libs.sh generates stub libs
(real SONAMEs) and the build links against them; the real driver loads at
runtime on NVIDIA hosts. Built into a separate target dir so it doesn't
clobber the software binary. Both ci.yml and the release gate clippy the
nvenc feature (via the stubs) so the hardware path can't rot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The release runner builds OpenH264's vendored C with whatever `cc` resolves
to (gcc or clang), but the stub generator hard-coded `gcc`, which isn't
present on a clang-only runner — so the nvenc clippy/build step failed. Use
`${CC:-cc}` (both compilers accept the flags) and fail loudly if absent.
Also add scripts/** to ci.yml's path filter so changes to the stub
generator actually get exercised.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The flagship 0.3.0 feature wasn't in the README intro or Features list —
only in the dedicated render section. Add it up top (intro line + a
Features bullet) so it's visible at a glance.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
release: v0.3.0-rc1 🧪
Some checks failed
ci / build-and-test (push) Has been cancelled
ci / build-and-test (pull_request) Successful in 8m28s
229bf59f29
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
psk merged commit 9a3a64d2f4 into master 2026-06-27 20:45:48 +00:00
psk referenced this pull request from a commit 2026-06-27 20:45:49 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
public/soundy-bits!6
No description provided.