Windows 11-ready WPF visualization of a 3D-stacked Game of Life grid. The UI stays minimalist-16:9 canvas, no chrome, with controls centered in the right-click context menu plus a dedicated Scene Editor for source stack workflows-and it supports tapping into open desktop windows, webcams, and media files as live depth sources.
Live authoring can use the video stack itself as the reactive soundtrack without playing that soundtrack through the speakers. Under Audio Source, choose Video Stack (Silent). Every enabled, playing video/YouTube/sequence layer with Play Audio on is decoded in real time, mixed with its per-layer and master source-audio volume, and fed directly into the normal level/beat/band/frequency analysis. LifeViz suppresses its source-audio output while this mode is selected, so another music app can keep playing independently. Switching back to a hardware input/output restores normal monitoring for the checked video tracks. This analysis-only selection is not muxed into ordinary recordings.
Live frame production is scheduled alongside WPF/D3D presentation work, keeping movie, simulation-group, and BPM-animation updates on a stable cadence even while the presentation surface is continuously invalidating.
Every source layer has a centered Scale control from 0.10× to 4.00×, available in both the Scene Editor and the right-click Sources menu. Rotate animations also have a persisted 0-360° Start Angle, applied before their clockwise/counterclockwise animated rotation. Both controls affect live playback and fixed-duration renders.
AutoClip layers add a symmetric Fade In / Out control (0-10 s); the effective fade is capped at half of a short clip so the two ramps meet cleanly. AutoClip also uses the same Mask / Keying (Normal) color, range, CPU/GPU compositing, and persistence path as ordinary file layers. The chroma-key range has a solid transparent interior and a narrow feathered boundary, avoiding translucent green-screen rectangles while preserving distant dark colors.
AutoClip chooses each duration before its random start point, clamps oversized requests for short sources, and leaves a small EOF decoder guard. Every scheduled clip therefore fits inside its source instead of reaching the final frame and appearing to pause.
Enable Loop selected file on an AutoClip to switch that scheduling behavior: the randomly chosen file starts at its beginning and loops for the entire chosen clip-time window, even when the window is longer than the media. When time expires it follows the same fade-out, optional delay, and random next-file cycle.
Each file inside an AutoClip can override the layer's compositing independently. Select a video in AutoClip Settings to choose an inherited or explicit blend mode and inherited/enabled/disabled chroma keying with its own color and range. This allows green-screen clips to use Normal + keying while already-matted or texture clips in the same AutoClip remain Additive.
Long-form output no longer has to be captured in real time. Right-click and choose Render Fixed Duration..., enter H:MM:SS (for example 5:00:00), and LifeViz renders the configured duration as quickly as the simulation, media decoders, and encoder can run. Choose a 30, 60, or scene-matching output FPS; 30 (long-form) is the default so a five-hour export does not accidentally inherit a 144 fps live scene and create 2.59 million frames. The simulation still advances at its configured rate between output frames. The modal render window shows frame progress, elapsed time, and an ETA, and it can cancel cleanly while preserving the partial video. Offline renders use the current recording quality and resolution/upscale and save beside normal recordings in %UserProfile%\Videos\LifeViz.
Offline renders use a deterministic frame clock for simulation, animations, GIFs, and file-video layers. File-video decoding is temporarily unthrottled, resampled to the export FPS, and consumed frame-by-frame so exports do not race past media. The main-window preview freezes on its last displayed frame while export is active, avoiding WPF/D3D presentation work; only the progress dialog refreshes. When export finishes, the live decoder resumes from its exact pre-render media position instead of jumping forward by the exported duration. Export work is scheduled independently of window focus: LifeViz temporarily uses above-normal process/UI-thread priority, offline ffmpeg decoders use normal priority, and each progress refresh uses a short asynchronous timer yield. While the dialog is active, LifeViz additionally drains WPF render/input work so the bar repaints and Cancel remains responsive. While it is inactive, dispatcher-priority waits are skipped because Windows defers them; the export keeps full throughput and the visible progress catches up when the dialog is activated again. All temporary priorities are restored after finalization. For reactivity, LifeViz ignores the selected live device and instead decodes and volume-mixes the embedded audio tracks from every enabled, unmuted video layer. That virtual mix drives level, beat, band, frequency, seeders, reactive mappings, and audio-synced animations on the export timeline. It is analysis-only: the rendered video remains silent for show playback, so add/mux a soundtrack afterward only if a separate audible copy is wanted. Desktop-window and webcam inputs remain live samples, so static/file-backed scenes are the best fit for deterministic faster-than-realtime rendering.
Performance-heavy scenes now avoid several sources of background contention and disk churn. Direct videos enter a nonblocking preparing state, while AutoClip and sequence metadata is probed lazily through one process-wide, two-process queue instead of scanning every listed file at startup; probe queue plus process time is bounded, successful local results are keyed to file metadata, failures retry after a backoff, and the cache is capped. Disabled sources stop capture/decode work; mixed-resolution GPU layers reuse a small upload-resource cache bounded by both entry count and 64 MiB; and desktop-window capture is paced at roughly 30 fps instead of running nearly unbounded. App configuration changes are coalesced for 500 ms and written atomically on one background writer, while session logging is buffered asynchronously, bounded, and capped at 8 MiB. The debug overlay text refreshes at 6 Hz and its FFT/history storage is reused, so enabling diagnostics no longer creates per-frame sort/string work plus per-audio-quantum allocation bursts.
dotnet build
dotnet runThe new Rider solution (lifeviz.sln) includes a "lifeviz: Run App" configuration so IDE runs mirror dotnet run.
For runtime validation without launching your saved scene manually:
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test gpu-benchmark
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test gpu-handoff
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test gpu-rgb-threshold
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test gpu-frequency-hue
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test passthrough-underlay-only
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test simulation-reactive-mappings
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test simulation-reactive-persistence
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test simulation-reactive-legacy-migration
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test simulation-reactive-removal
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test simulation-reactive-editor-isolation
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test pixel-sort-editor-roundtrip
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test no-sim-group-renders-composite
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test sim-group-removal-clears-runtime
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test disabled-sim-group-renders-composite
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test sim-group-stack-order
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test sim-group-inline-hue
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test sim-group-inline-presentation
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test gpu-pixel-sort
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test sim-group-pixel-sort-color
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test gpu-bitwise
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test gpu-injection-mode
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test gpu-file-injection-mode C:\path\to\video.mp4
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test offline-video-audio C:\path\to\video-with-audio.mp4
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test live-video-audio C:\path\to\video-with-audio.mp4
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test gpu-sim
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test gpu-source
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test source-reset
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test layer-transform-controls
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test chroma-key
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test gpu-render
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test profile-mainloop
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test profile-mainloop-sim-group
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test profile-240
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test profile-480
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test profile-rgb-240
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test profile-rgb-480
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test profile-file-240 C:\path\to\video.mp4
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test profile-file-480 C:\path\to\video.mp4
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test profile-file-rgb-240 C:\path\to\video.mp4
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test profile-file-rgb-480 C:\path\to\video.mp4
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test profile-current-scene
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test profile-current-scene-visible
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test profile-current-scene-fullscreen
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test profile-current-scene-bisect
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test profile-current-scene-720
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test profile-current-scene-visible-720
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test profile-current-scene-fullscreen-720
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test profile-current-scene-presets
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test profile-current-scene-visible-presets
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test profile-current-scene-fullscreen-presets
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test profile-current-scene-interaction
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test pacing-current-scene-visible-presets
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test pacing-current-scene-fullscreen-presets
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test pacing-current-scene-interaction
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test pacing-current-scene-overlay-fullscreen-144
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test pacing-current-scene-suite
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test frame-pump-thread-safety
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test shutdown
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test startup
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test config-save-coalescing
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --smoke-test allThe live renderer now keeps source-layer sampling pixel-sharp by default on the GPU path too; the source compositor uses point sampling rather than linear filtering unless a future explicit smoothing control is added.
Inline Sim Group scenes still use the shared-GPU presentation path, and the renderer now logs a throttled warning if that inline GPU handoff falls back to the CPU-present path so live flicker reports can be diagnosed from the real app log instead of by guesswork.
For visible normal-startup diagnostics against your real saved scene instead of smoke-mode startup:
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --diagnostic-test profile-current-scene-visible-144
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --diagnostic-test profile-current-scene-visible-480
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --diagnostic-test profile-current-scene-visible-720
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --diagnostic-test profile-current-scene-interaction
dotnet bin\Debug\net9.0-windows-sbx\lifeviz.dll --diagnostic-test profile-current-scene-soak-720gpu-bitwise is the dedicated smoke for the new Bitwise RGB life mode. It validates that the GPU path can inject a live RGB composite into 24 Conway bit planes and reconstruct the expected RGB byte pattern from those planes.
gpu-benchmark reports GPU sim/source timings for the current synthetic workload so you can see whether readback is still dominating. gpu-handoff validates that a GPU-built composite can inject directly into the GPU simulation path without a CPU composite readback. gpu-rgb-threshold specifically validates the RGB composite-injection threshold path against a pure-white source, which catches GPU handoff regressions where the compositor texture is sampled incorrectly or left bound as a render target. gpu-passthrough-signed-model verifies that passthrough uses the signed additive/subtractive composition model whenever a passthrough baseline is present, including the shared-GPU underlay path, so layer opacity changes cannot incorrectly fade the underlay. passthrough-underlay-only now drives the real frame tick and verifies that with zero active simulation layers, source capture/composite refresh still happens and passthrough still presents the source composite instead of falling to black. gpu-frequency-hue validates that per-layer frequency-driven hue shift numerically changes the resolved hue handed to presentation while the raw RGB simulation buffer itself stays unchanged in the live GPU path. simulation-reactive-mappings validates the modular per-layer mapping system itself, including each mapping row's input Min / Max threshold window normalization, against known audio inputs even when the legacy global audio-reactivity master toggle is off. pixel-sort-reactive-cell-size validates the Cell Width / Cell Height reactive outputs against a live GPU Pixel Sort layer and fails unless the runtime cell size actually changes while the sorted output still preserves the source histogram. simulation-reactive-persistence round-trips those mappings through both the scene-project serializer and the app-config normalization path so per-layer reactivity does not silently disappear on save/load. simulation-reactive-legacy-migration verifies that old whole-scene Level -> Framerate and Level -> Life Opacity config flags are migrated into per-layer mappings and then cleared, so the app no longer relies on the split global path. simulation-reactive-removal verifies that removing a mapping really returns the layer to its base state instead of leaving stale reactive output behind. simulation-reactive-editor-isolation verifies both that newly added simulation layers start with an empty reactive list instead of inheriting the selected layer's mappings and that the live sim-group Selected Layer editor still exposes the Reactive Mappings UI for the selected child layer. pixel-sort-editor-roundtrip specifically verifies that a Pixel Sort layer survives the Scene Editor add/apply/refresh path with its layer type and cell-size settings intact, so editor rebinding cannot silently downgrade it back to Life Sim. no-sim-group-renders-composite verifies that when there are no scene Sim Group layers, the app presents the source composite directly instead of leaving a blank frame or a hidden runtime sim stack. sim-group-removal-clears-runtime verifies that removing the last scene Sim Group actually clears the runtime simulation stack instead of leaving a hidden stack running. disabled-sim-group-renders-composite verifies that even when a Sim Group still exists in the scene, disabling all of its child sim layers falls back to the source composite instead of leaving stale simulation output onscreen. sim-group-stack-order now verifies both that moving the same sim group to a different position in the main scene stack changes the resolved output and that the resolved inline scene composite stays GPU-backed. sim-group-inline-hue verifies that the inline sim-group compositor still applies per-layer RGB hue shift while remaining GPU-backed, so moving sim groups into the main stack does not silently bypass the live hue controls. sim-group-inline-presentation now uses the minimal repro stack for the flicker bug: rainbow background, inline Sim Group with Pixel Sort, and a static portrait-style source above it at 240p. It now has a dynamic phase that requires the presented output to keep advancing with the evolving scene, a static phase that fails unless the presented frame stays bit-stable for the whole dwell window, a redraw-pressure phase that hammers chrome/UI invalidation while manual redraws and frame submits keep running, a real file-backed 480p hover phase, and an exact-repro phase that uses the copied rainbow JPG plus converted Mona Lisa PNG from Assets/SmokeRepro. That exact phase keeps the three-layer stack static at 480p, fails unless both the resolved inline composite and the presented frame stay bit-stable through hover pressure, and now also asserts that the inline scene is being handed to presentation through the dedicated GPU snapshot ring instead of directly through a live source-compositor shared texture. gpu-pixel-sort validates the new GPU pixel-sort layer backend directly by injecting a live GPU composite, resolving the cell sort on-GPU, and failing unless the resolved output surface actually differs from the input pattern while preserving the pixel histogram. sim-group-pixel-sort-color pushes a Pixel Sort layer through the real inline sim-group renderer and fails unless the resolved group output still preserves the source histogram instead of blowing out colors by re-adding the underlay or sampling the wrong GPU texture format. gpu-injection-mode validates that Threshold, Random Pulse, and Pulse Width Modulation still produce distinct densities on a mid-gray source in the GPU composite-injection path. gpu-file-injection-mode <video> runs that same density comparison against the first decoded frame of a real file source, which is useful when a report only reproduces on actual media. gpu-sim validates the D3D11 simulation backend in both Naive Grayscale and RGB Channel Bins. gpu-source drives the real BuildCompositeFrame path through MainWindow with synthetic sources and fails unless the GPU source compositor actually runs. Static file-backed images no longer opt into the native-source fast path; they stay on the exact CPU-downscaled BGRA path so hover/input redraws cannot destabilize them and they keep the default pixel-sharp look. gpu-render launches a hidden MainWindow and fails unless the real GPU composite pipeline initializes through the app render path. profile-mainloop runs the real frame loop on a hidden synthetic scene, exports a JSON timing report under %LOCALAPPDATA%\lifeviz\profiles, and logs the highest-cost stages plus UI dispatcher latency samples and frame-gap spike counters (>25ms, >33ms, >50ms). profile-mainloop-sim-group runs that same profiler against a synthetic scene with an embedded Sim Group, so inline-stack compositing cost can be compared directly against the plain-source baseline. Profile and pacing smokes no longer request CPU fallback color buffers from GPU simulation layers, and the sim-group profiling path now also suppresses intermediate sim-group readback, so those timings stay close to the live shared-texture renderer instead of the correctness-validation path. profile-240 / profile-480 do the same at fixed grayscale resolutions, while profile-rgb-240 / profile-rgb-480 pin the reference layer to RGB Channel Bins so renderer changes can be measured against the formerly slow path directly. profile-file-240 / profile-file-480 run that same profiler against a real file-backed source instead of synthetic buffers, and profile-file-rgb-240 / profile-file-rgb-480 do the same with the reference layer pinned to RGB Channel Bins; pass the media path as the third argument (or set LIFEVIZ_SMOKE_VIDEO). Those file-backed smokes now fail if a file source stops publishing fresh frame tokens, which catches non-exact video handoff regressions automatically. profile-current-scene loads the persisted user scene/config and profiles it headlessly, while profile-current-scene-visible does the same in a visible window so visible-window pacing regressions can be measured without hand-copying overlay text. profile-current-scene-bisect runs the same real-scene visible profile several times from fresh startup variants (baseline, no-audio, no-video, no-sim-groups, and first-static-only) so config-dependent regressions can be narrowed to audio loopback, video decode, inline sim groups, or source complexity instead of being treated as one opaque “current config” problem. These current-scene profile smokes now wait through an explicit warmup period before they start measuring, and then fail on settled playback freshness (capture_*_frame_age_ms, capture_*_fresh_frame_ratio) instead of trusting startup-noisy averages. profile-current-scene-fullscreen does the same after forcing the main output window into fullscreen and now also fails if the render host collapses into the old tiny centered/top-centered rectangle instead of occupying the expected fullscreen display rect. profile-current-scene-<144|240|480|720|1080|1440|2160>, profile-current-scene-visible-<...>, and profile-current-scene-fullscreen-<...> force the real saved scene to that exact preset height before profiling, and the profile-current-scene-presets / profile-current-scene-visible-presets / profile-current-scene-fullscreen-presets suite targets walk the full preset ladder up to 2160p using a short per-preset dwell so the full sweep is practical to run regularly. profile-current-scene-interaction now opens the Scene Editor, verifies that the main window does not stay throttled while the editor is open, cycles the root context menu, and fails unless pacing stays near the pre-interaction baseline after both operations. current-scene-hover-presentation loads the saved scene, applies hover-style chrome invalidation pressure while the live frame pump is running, and compares presented-frame delta statistics against a baseline window. When the saved scene contains inline Sim Group layers, it now also asserts that the stable GPU snapshot ring was actually exercised during the run. pacing-current-scene-visible-presets and pacing-current-scene-fullscreen-presets take the real saved scene, pin it to a stable 60 fps budget, walk the realtime preset ladder (144/240/480), and fail on explicit underrun thresholds (avg/p95/p99 frame gap plus >25ms, >33ms, >50ms ratios) instead of just exporting timing data. pacing-current-scene-interaction does the same around the 480p interaction case with the Scene Editor and root context menu. pacing-current-scene-overlay-fullscreen-144 turns on Show FPS in fullscreen at 144p and fails if the overlay itself reintroduces pacing regressions. pacing-current-scene-suite runs the visible preset ladder plus the interaction case in one go. frame-pump-thread-safety opens the real Scene Editor, hits the frame-pump interaction helper from a worker thread, and fails on the exact cross-thread Window.IsActive regression that crashed the app. In smoke-test mode, profile reports are written under bin\Debug\net9.0-windows-sbx\profiles\ so they stay local to the test run. shutdown now opens the Scene Editor and then closes the real main window, failing if shutdown teardown captures any exception or if the owned editor-close path throws. startup launches a minimal WPF window in smoke-test mode without loading the persisted project, media sources, ffmpeg decode, or audio loopback so UI/render initialization can be tested in isolation. all runs gpu-sim plus the combined GPU handoff/passthrough/source/render UI smoke suite.
--diagnostic-test ... is the complement to smoke tests: it launches the normal saved-scene startup path instead of App.IsSmokeTestMode, writes reports under the build output's profiles\ folder, and is intended for live-only divergences where smoke mode looks clean. The current targets cover visible preset runs, fullscreen preset runs, interaction profiling, and a longer 720p soak. Settled profile validation now also checks presentation_draw_fps, so a run can fail when the frame loop looks healthy but the actual presented cadence falls behind. In the running app, Show FPS now reports Present, Loop, and Sim separately, and the root context menu includes Export Live Profile... to capture the exact live session without restarting into the saved scene.
Realtime media workers and ffmpeg child processes now run at normal priority by default so video frames and the silent video-stack audio feed cannot be starved behind rendering work. Low Contention Mode remains the explicit opt-in for yielding decode work to other realtime apps and USB/audio stacks.
The root context menu includes Performance controls for live contention tuning: Low Contention Mode lowers LifeViz's own process/frame-pump priority, stops requesting the global 1 ms multimedia timer, and moves ffmpeg decode processes toward background scheduling; Decoder Threads lets you cap ffmpeg decode fanout (Auto, 1, 2, 4), with Auto bounded to two codec threads per live video; and Video Decode FPS lets you cap file/video decode output to 30 or 15 fps when you need LifeViz to back off while sharing a machine with DJ/controller software. Those settings apply to existing video/sequence sessions immediately. Offline rendering retains full automatic decoder fanout for export throughput.
dimensions now covers the real Scene Editor height dropdown in both Live Mode and deferred Apply mode with the reference simulation layer forced to RGB Channel Bins, and fails unless every simulation layer plus the presentation surface resize to match.
For a quick local install/update smoke test:
.\install.ps1By default, install.ps1 now runs Install-ClickOnce.ps1 against the fresh publish output, stages the payload under %LOCALAPPDATA%\lifeviz-clickonce, removes stale ClickOnce .appref-ms Start Menu entries, and creates normal LifeViz Start Menu/Desktop shortcuts that launch the staged lifeviz.exe directly. Use -BundleInstaller if you explicitly want a generated single-file lifeviz_installer.exe.
Right-click the scene and use Sources to stack multiple windows, OBS-style:
- Add entries via Sources > Add Window Source, Add Webcam Source, Add File Source, Add Video Sequence, or Add Layer Group (checked items are already in the stack). File sources accept static images (PNG/JPG/BMP/WEBP), GIFs, and videos; animated files loop. Video sources always loop, and video sequences build a playlist that advances on end and loops back to the first clip. Video layers expose Restart, Pause/Play, and Scrub controls so you can seek directly to a position. Layer groups have their own submenu stack; they composite their children internally and blend as a single layer, using the first child to determine the group's aspect ratio. The top entry is the primary: it drives the canvas aspect ratio (unless the aspect ratio lock is enabled). Make Primary, Move Up/Down, or Remove/Remove All to resequence quickly; clearing all sources drops back to the 16:9 default.
- Video, video-sequence, and YouTube layers include a Play Audio toggle (default off) plus a per-layer Audio Volume control. Enable audio per source when you want that layer's soundtrack routed to your default output device, then trim that source independently. Sources without an audio stream stay silent and log a warning instead of continuously retrying playback.
- AutoClip is a first-class randomized video layer available from the Sources menu and the Scene Editor's root/child add rows. It starts empty; select it in the Scene Editor to add or remove videos and enter minimum/maximum clip length plus minimum/maximum delay in seconds. Each cycle chooses a random listed video, random point in its timeline, and random duration. A non-zero delay makes the layer transparent/off until the delay expires; zero delay switches directly. AutoClip supports the same per-layer audio toggle/volume and silent video-stack reactivity as other video layers, while its transport is managed automatically.
- Audio playback is lifecycle-managed per source: muting a layer, removing it, or closing the app now shuts down that source's in-app audio pipeline.
- Audio playback startup is debounced per source so rapid state changes do not spawn duplicate decode/playback pipelines.
- Enabling Play Audio now seeks to the current video playback position (best effort), so toggling audio on mid-playback stays close to visual timing instead of always starting at 0:00.
- Audio decode is clocked in real-time to avoid fast decode churn/restarts that can cause stuttery playback artifacts.
- Each source exposes a wallpaper-style fit mode (Fill default, plus Fit/Stretch/Center/Tile/Span) that controls how the layer scales into the frame.
- Each source has its own blend mode applied during compositing (Additive default; Normal, Multiply, Screen, Overlay, Lighten, Darken, Subtractive). Normal respects per-pixel transparency in sources like PNGs, and can optionally key out a background color (default black) with an adjustable range.
- Scene Editor... opens a dedicated two-pane source manager with a draggable nested tree on the left and tabbed controls on the right. The main scene tree now supports a
Sim Grouplayer type directly alongside source layers and layer groups. When aSim Groupis selected, its nested simulation-layer tree and per-layer simulation settings live on the Selected Layer tab instead of in a separate global simulation-stack editor. The secondary simulation tab now focuses on project-wide controls (Height,Depth,Framerate, andGlobal Life Opacity). Sim groups define ordered chaining: the first child samples the scene-stack surface that exists at that exact point in the main layer order, and each later child samples the previous child output. After the group resolves its own sim layers, lower scene layers composite on top of that fully rendered group output, so moving a sim group up or down the main stack now changes both its input and everything below it. On the live GPU path, that inline sim-group resolution now stays GPU-backed end-to-end: source layers append onto the current scene composite on the shared D3D11 device, sim-group children resolve into an offscreen GPU composite surface, and the final scene composite is presented from that GPU surface without CPU readback except for recording/smoke validation paths. New sim groups now start empty, so you explicitly choose whether the group containsLife Sim,Pixel Sort, or a mix. Inside the sim-group editor, the add controls are nowAdd Life SimandAdd Pixel Sort Layer.Life Simkeeps the Conway settings (Input,Injection,Life Mode,Binning,Injection Noise, threshold window).Pixel Sortis a GPU-only per-frame cell sort layer with its ownCell WidthandCell Heightsliders;1x1is effectively passthrough, and larger cells target a full top-left-to-bottom-right sort order inside each cell while preserving the sampled pixel shades. Pixel Sort is treated as a full-frame effect layer, so once it has baked the upstream scene into its own output the sim-group compositor does not add the original underlay back on top again. Both layer types keep commonBlend,Opacity, hue, andReactive Mappingscontrols. Each reactive mapping now includes an audio input, an output, an amount, and per-mapping inputMin/Maxthreshold sliders. The incoming audio value is normalized through thatMin/Maxwindow before it drives the output. For Pixel Sort,Cell Width/Cell Heightmapping amounts are additive deltas from the base cell size, shown as+N px, and capped at50 pxso the effect stays in the useful range for this filter. New simulation layers start with an empty reactive list instead of inheriting the selected layer's mappings, and cleared mappings now also clear the old hidden legacy hue-reactivity field on live apply/save so deleting the last mapping really removes the effect. Height is preset-driven (144,240,480,720,1080,1440,2160) in both the main menu and Scene Editor, and the Scene Editor dropdown now uses an explicit dark selection/hover style so preset rows stay readable while you scrub through them. The editor includes an App Controls... button that opens the full main context menu at the editor location for global settings parity. For video-capable layers it adds a scrub slider with live time readout so you can seek playback position. The Scene Editor header also provides global source-audio controls: Master Audio (toggle) and a master volume slider that scale/mute all video/YouTube/sequence audio without changing per-layer settings. Enable Live Mode for immediate updates, or turn it off and use Apply to batch changes. The editor includes Save... and Load... buttons to export/import.lifevizlayers.jsonscene projects; these files now include sim groups inside the main scene stack, per-group simulation-layer trees, source stack, per-layer reactive mapping lists, and core simulation/render project settings (height, depth, framerate, global life opacity, passthrough/composite blend, invert composite). - Legacy global simulation config is no longer honored on normal startup. Simulation now only comes from real scene-stack
Sim Grouplayers, and loading an old legacy-only config clears that legacy path on the next save. - When there are no scene
Sim Grouplayers at all, or when every child layer inside the existing sim groups is disabled, LifeViz presents the composited source stack directly instead of falling back to a hidden/default simulation stack, stale simulation frame, or a blank output. - Clearing all scene sources no longer forces Passthrough Underlay off, so re-adding sources preserves the previous underlay display mode instead of coming back black until you manually re-enable passthrough.
- Each source can stack animations (Zoom In, Translate, Rotate, Beat Shake, Audio Granular, Fade, DVD Bounce) synced to the global animation BPM, with forward or reverse loops plus expanded speed steps (1/8x–8x) and per-animation cycle lengths for long fades; Beat Shake responds to detected audio beats when a device is selected (falls back to BPM if not) and includes an intensity slider (speed/cycle do not affect its amplitude), Audio Granular now uses a gated/compressed response curve with a per-layer 3-band EQ (Low/Mid/High) and a wider intensity range up to 1000% (with a stronger neutral 100/100/100 EQ baseline), and DVD Bounce exposes a size control. DVD Bounce translation is now snapped to whole pixels on the point-sampled render path to reduce shimmer/flicker on moving photographic layers without turning layer smoothing back on by default. Use Animation BPM > Sync to Audio BPM to beat-match all animations, and Audio Source > None to clear the input.
- Audio Reactivity adds configurable simulation modulation from the selected audio source: simulation layers now own the canonical modular reactive-mapping system in the Scene Editor. Supported per-layer inputs are
Level,Low,Mid,High, overallFrequency,Low Frequency,Mid Frequency, andHigh Frequency. Supported per-layer outputs areOpacity,Framerate,Hue Shift,Hue Speed,Injection Noise,Threshold Min,Threshold Max,Cell Width, andCell Height. Every mapping row now also has inputMin/Maxthresholds, and the incoming audio value is normalized through that window before the output amount is applied. For Pixel Sort,Cell Width/Cell Heightamounts are additive deltas from the base value, displayed as+N px, and capped at50 px. Band-energy signals are normalized logarithmically to0..1inside their own frequency bins, and band-frequency signals are log-normalized within those bins as well so the mapping feels musically sensible instead of linear in Hz. Per-layer mappings are keyed off the selected audio device and do not depend on the legacy global audio-reactivity master toggle. The context menu now keeps only the genuinely global audio actions: input gain, level seeding, and beat seeding. Old whole-sceneLevel -> Framerate/Level -> Life Opacitysettings are migrated forward into per-layer mappings automatically. Audio Source includes output devices (includingSystem Output (Default)via WASAPI loopback) plus input devices. - Context menu responsiveness: the Sources and Audio Source submenus now refresh device lists when those submenus are opened, instead of doing that work every time the root menu opens.
- Audio level normalization is tuned for typical program material instead of near-full-scale samples, so loopback audio reaches a much larger portion of the 0-100% range and opacity/framerate reactions read more strongly at normal listening levels.
- Audio level percent shown in the FPS overlay now uses that rectified short-window max envelope directly, so it tracks the absolute waveform peaks per bucket instead of averaging them down.
- Video file sources are decoded directly with
ffmpeg; installffmpegonPATH. LifeViz does not create a persistent transcode cache, so files that the installed decoder cannot read should be transcoded manually. - Composite Blend controls shader-based passthrough fallback mixing (Additive default; Normal is transparency-aware).
- Passthrough Underlay shows the composite behind the simulation.
- The root Simulation Layers submenu now exposes simulation-layer management plus a global master Life Opacity control. Per-layer life mode, binning mode, injection noise, and layer opacity now live in the Scene Editor's Simulation Layers panel.
- Life Opacity now attenuates simulation output only; passthrough underlay remains visible at full strength even at 0% life opacity.
- If reactive opacity drives every simulation layer to 0% on a frame, the GPU renderer now keeps presenting passthrough as an underlay-only frame instead of dropping out to a black fallback frame.
- Source capture/composite refresh is now decoupled from simulation stepping, so passthrough and file/video underlay keep updating every render tick even when there are no enabled simulation layers or when a layer is stepping more slowly than the render loop.
- Shared GPU sim/underlay presentation now performs one synchronized producer flush at the final presentation handoff instead of per-layer flushes. That keeps the live shared-texture path visually current without going back to the old worst-case flush-per-layer behavior.
- Fullscreen toggle lives in the context menu and persists; it now sizes to the active monitor bounds, stays topmost, and covers the taskbar.
- The main output window now uses custom chrome instead of the native Windows title bar. The title shows the running semantic release (for example
LifeViz v4.4.1) or a short commit-backeddevidentity for local builds, so screenshots and bug reports identify the actual executable. Dragging the background anywhere in the output window now moves the window, double-clicking that background toggles fullscreen, and resizing from the left/right/bottom hit zones stays inside LifeViz pointer handling instead of entering the OS move/resize loop. The top chrome buttons expose the root menu (...), Scene Editor (SE), minimize, fullscreen, and close. The main render loop no longer self-throttles just because the root context menu or Scene Editor is open; throttling is now reserved for actual native move/resize loops. - Frame pacing is now driven by a dedicated high-resolution scheduler thread rather than
System.Threading.Timer. The scheduler posts at most one pending frame callback to the WPF dispatcher at a time, which substantially reduces baseline underruns and timer-queue jitter even before any mouse interaction occurs. - Entering or exiting fullscreen now explicitly resets frame-pump cadence, so the scheduler does not carry stale timing state across a display-mode transition.
- Custom resize now owns aspect enforcement directly while you drag, so the window no longer snaps back after release. The generic WPF
SizeChangedaspect snap no longer fights the chrome resize path. - The root menu repeats the Current version beside Update to Latest Release... for fullscreen access. Updates pull down the newest GitHub release installer, close LifeViz, wait for the running staged executable to release its files, and then replace the installation in place.
- Capture uses DPI-correct window bounds (via DWM) so the full surface is normalized even for PiP/scaled windows, and the composited buffer feeds the injection path (per-layer threshold windows + noise + life/binning modes) on every tick.
- Source downscale now uses small supersampled filtered sampling before injection to reduce visible vertical/horizontal banding in live imagery. The CPU compositor keeps cheap aligned copies for normal cases and only uses filtered remapping when needed, while the final life grid still presents with nearest-neighbor scaling so cells stay crisp.
- Renderer migration now includes a real GPU presentation/composite path.
MainWindowtalks to anIRenderBackend; the default path attempts aGpuRenderBackendfirst, hosting a D3D11-backedDrawingSurface(Vortice.Wpf), uploading the final simulation frame plus passthrough underlay into GPU textures, and resolving passthrough blend modes in a fullscreen shader pass before presentation. Source capture compositing now also prefers a D3D11 path: source frames are uploaded as textures, blended on a GPU render target with fit modes/transforms/keying/blend modes preserved, then the final composite is read back once for injection. CPU source compositing remains as automatic fallback/migration scaffolding, but simulation itself is now GPU-only and startup fails if the D3D11 simulation backend cannot initialize. - The live render surface is now treated as display-only by WPF. The
Viewboxhost, fallbackImage, and GPUDrawingSurfaceare non-hit-testable/non-focusable so ordinary mouse hover over the window does not route interaction through the render surface itself. - The final render host no longer routes the GPU/D3D surface through a WPF
Viewbox. The fallbackImageand GPUDrawingSurfacenow scale withStretch=Uniforminside a plain black host instead, which removes a large-window/fullscreen WPF scaling cost that could make the underlay video look like a slideshow even when simulation timing was fine. - Simulation layers now sit behind an
ISimulationBackendseam, and the default backend is nowGpuSimulationBackendwhen possible. BothNaive GrayscaleandRGB Channel Binsnow run their injection, Conway stepping, depth-history shifting, and color-buffer generation in D3D11 compute shaders. RGB layers can inject directly from the final GPU composite too, including direct/inverse input mapping, threshold/random pulse/PWM injection modes, per-layer threshold windows, injection noise, and hue-rotated channel mapping. - Runtime smoke tests are now first-class:
--smoke-test gpu-benchmarkprofiles the current GPU sim/source stack,--smoke-test gpu-handoffverifies direct GPU composite-to-sim injection without CPU composite readback,--smoke-test gpu-simexercises the GPU simulation backend directly,--smoke-test gpu-sourceverifies thatMainWindowreally uses the D3D11 source compositor forBuildCompositeFrame,--smoke-test gpu-renderverifies thatMainWindowreally brings up the D3D11 presentation composite path,--smoke-test profile-mainloopcaptures full frame-stage timing plus UI dispatcher latency samples and frame-gap spike counters from the real frame loop,--smoke-test shutdownverifies that the real window can tear down cleanly even when the Scene Editor is open, and--smoke-test startupopens the app in an isolated smoke-test mode that skips persisted scene/config startup so renderer/UI regressions can be caught without spinning up user media pipelines. - File-backed smoke profiling is now supported:
--smoke-test profile-file-240 <video>andprofile-file-480 <video>run the real frame profiler against an actual media file source, which is the right way to measure decode/playback regressions that synthetic scenes cannot reproduce. Those smokes now fail if the file source stops advancing fresh frame tokens, so a frozen underlay video is caught automatically instead of only by eye. - Audio analysis is now demand-driven: waveform history, bass/mid/high frequency extraction, and 30-second debug traces only run when the FPS/debug overlay is visible or when a live feature actually needs spectrum data (for example
Audio Granularor a per-layer reactive mapping that consumes frequency/band inputs). - When spectrum analysis is required, FFT/spectrum extraction is now rate-limited to about
60 Hzinstead of running on every audio callback. That keepsAudio Granularand per-layer frequency mappings responsive without letting the audio thread dominate low-resolution scenes. - File-video playback no longer hands the UI a single latest-frame slot.
VideoSessionnow keeps a short decoded-frame queue plus buffer pool, which smooths over brief UI/render stalls instead of immediately turning them into visible dropped frames. - Long-lived file-video raw decode, sequence disposal, and source-audio decode workers run off the UI thread. Direct-video initialization is pending/nonblocking, and metadata probes use a capped shared asynchronous cache with at most two
ffmpegprobe processes at once and a 15-second total queue/process deadline. Local file size/write time invalidates stale successes; failed probes retry after a short backoff instead of remaining permanently poisoned. - File-video decode resolution is now adaptive. When the render path does not need native source pixels, LifeViz restarts
ffmpegat a process resolution sized to the current simulation grid instead of always pushing native-sized raw frames through the app. That specifically targets the "smooth sim, slideshow video" failure mode on low-resolution scenes with HD sources. - File-video decode no longer emits periodic per-frame heartbeat logs during normal playback, and tiny file-video downscales now stay single-threaded instead of spawning
Parallel.Forwork. That reduces long-run threadpool churn and low-resolution playback collapse in file-backed scenes. - On the GPU source-composite path, low-resolution scenes no longer prefer native file/video frames by default. For grids below roughly
640x360, LifeViz uses the already-downscaled file frame instead of hauling full native video frames through the compositor, which substantially reduces CPU/memory pressure for dense low-res multi-video scenes. - Native-source preference is now media-aware. Static file sources can still switch back to native frames at moderate sizes, but video file sources and video sequences stay on adapted/direct decoder output at every preset height. For the live multi-video scene, switching video back to native frames was the point where
1080p+freshness collapsed into slideshow behavior. - File-video downscale now has direct-copy and centered crop/pad fast paths for the common low-resolution adapted-video cases (
source == target, vertical crop, horizontal crop, centered letterbox/pillarbox). The profiler also records delivered file-frame freshness (capture_file_fresh_frame_ratio) and age (capture_file_frame_age_ms) so stale underlay video can be diagnosed directly instead of inferred from render timing. - When a video source does not need native source pixels and its fit mode is
Fill,Fit, orStretch, LifeViz asksffmpegto emit exact target-sized raw frames directly only when that does not require decoder-side upscaling past the native source size. That preserves the low-resolution freshness win without turning1080p+and4Kruns into giant decoder-side upscale workloads. - File-video sessions now also publish their current decoder-sized frame directly into the compositor path and let the GPU scale it to the final composite size. That removes the CPU resample worker from the normal live video path even when the decoder is using an adaptive/native-sized frame instead of exact target output, which is the key to making
1080p+and4Kprofiling credible. The capture path now preserves those decoder-sized buffers all the way throughCaptureFrame()instead of discarding them for not matching the requested target size, which fixes the freeze that could appear when a video source switched from exact-target decode to adaptive/native decode. - When a file-video pipeline is restarted for a new target/process size, LifeViz now holds the last good decoded frame until the new worker publishes replacement frames. That prevents the old immediate black flicker during height changes such as
480p -> 1080p. - File-video frames now carry their true published buffer dimensions through the capture path even when the decoder is not emitting exact target-sized output. Without that metadata fix, the compositor could treat a native/adaptive video frame as if it were already target-sized and appear to freeze or smear only on the non-exact video path.
- Current-scene profiling now records per-source file freshness metrics as well as the aggregate ones. That makes it possible to tell when one file source is freezing while another keeps updating, which is exactly the failure mode that aggregate
capture_file_frame_age_mscan miss. - File-video sessions now defer starting their
ffmpegdecode worker until the first real capture request. That removes the old probe-time native-worker startup followed by an immediate restart into the actual process shape, which was a bad fit for mixed-resolution scenes and short looping clips. - Live file-video decoding now bounds
Autoto two codec threads per decoder, runs scaling through one filter thread, maps only the required video stream, and retains at most two pending raw frames. This prevents multi-video scenes from multiplying unrestrictedffmpegthread pools or building roughly half-second BGRA backlogs that surface as stale/jumping movie playback. Explicit decoder-thread settings still override the live default; offline rendering keeps full decoder fanout and its deeper deterministic queue. - Video-sequence layers keep the last good frame visible while the next clip session is opened asynchronously. Sequence handoff is still a cold open today, but the underlay no longer drops to black just because the next clip has not published its first frame yet.
- AutoClip handoffs now track which file owns the retained display frame separately from which file the scheduler has selected next. While the next decoder warms up, the previous frame therefore keeps its own blend/key override instead of briefly receiving the new clip's override and flashing keyed colors such as green.
- Sequence and AutoClip clips request probe metadata only when a clip is actually selected. Live capture enters a nonblocking preparing state and promotes the clip on a later tick; deterministic offline rendering waits on that same cached task under one handoff-wide deadline, then skips an unprobeable source for that render instead of spending another full timeout on each later frame/path.
- GPU source uploads reuse up to eight D3D11 texture/SRV pairs by source dimensions, subject to a 64 MiB total cache budget. Alternating layers such as
1280x720 -> 640x480 -> 1280x720therefore stop destroying and recreating GPU resources every frame without retaining eight full-resolution textures unconditionally. - Disabled source layers and disabled parent groups are skipped before capture and compositing. Video/AutoClip/sequence decoders are suspended without changing the user's explicit play/pause state, and window/webcam sessions are released without synchronously waiting on an in-progress GDI capture.
- File and window capture hot paths no longer maintain live metrics, freshness metric names, or mapping tables when neither profiling nor Show FPS needs them. Window capture sessions are paced at about 30 fps to avoid saturating a CPU core with
PrintWindow/BitBlt polling. - Session logs use a nonblocking 512-record queue and one below-normal writer thread, flush at most every 500 ms, and stop file output at 8 MiB per launch. Repeated offline-audio timestamp warnings were fixed at the decoder command as well, so the logger is no longer fed tens of lines per second during long exports.
- The GPU final-composite path no longer forces redundant producer-device flushes before every present. That was the main cause of the render-side
720pstalls after the sequence path was fixed, because it converted otherwise cheap queued GPU work into a synchronous wait on every frame. - Simulation injection now lazily rebuilds a CPU-readable composite only when GPU injection cannot satisfy a layer on that frame, so turning passthrough/recording off no longer risks an empty CPU fallback composite and lost injection.
- Final simulation-layer compositing now prefers a dedicated GPU presentation pass when not recording, so the old CPU per-pixel layer blend is mostly a fallback path instead of the normal render hot loop.
- Naive-grayscale GPU simulation layers now expose their rendered color textures to the presentation backend through shared D3D11 resources, so the normal GPU presentation path can sample those layers directly without a per-frame CPU
FillColorBuffer()readback. - On the normal GPU live path, passthrough underlay now rides the shared GPU composite surface directly instead of forcing a CPU composite readback just to feed final presentation. CPU composite readback is still required for recording and for any simulation layer that falls back to CPU mask injection on that frame.
- The main frame loop uses a coalesced high-resolution frame pump that schedules the next UI-thread callback at the current target cadence instead of spamming 1ms dispatcher ticks. The dispatcher callback runs at render priority, and cadence state is explicitly reset after context-menu/native move-loop interaction and after simulation-height changes so temporary UI throttling does not leave the renderer in a degraded pacing phase.
- The main output window no longer relies on native title-bar dragging for its normal shell interaction. Custom chrome/manual resize keeps ordinary window moves out of the OS move loop, so render pacing is not forced to compete with native non-client dragging in the same way. Title-bar/menu pacing smoke coverage is now focused on the custom root-menu interaction path.
- Double-clicking the output surface now toggles fullscreen in both directions, so you can enter or leave fullscreen without using the context menu.
- Naive-grayscale GPU simulation now shares the same D3D11 device/context as the GPU source compositor, so grayscale layers can inject straight from the final GPU composite texture without rebuilding CPU masks first.
- The legacy CPU simulation fallback has been removed.
GpuSimulationBackendis now the only simulation backend, and the old CPU-oriented runtime fallback path no longer exists inside simulation startup or stepping. - Simulation output is now a user-managed stack of simulation layers and sim groups. Top-level leaf layers still inject from the final source composite. A sim group chains its children in order: the first child samples the group's incoming surface, each later child samples the previous child output, and the group publishes its last child as the downstream surface. Catch-up steps after the first injected pass stay step-only, so source injection still happens once per render frame.
- Injection now modifies the current simulation generation before the normal Conway step instead of inserting an extra duplicate history frame, which keeps gliders and ships visually crisp rather than fattening their trails.
- Default layer set is two entries: Positive (Direct + Additive) and Negative (Inverse + Subtractive), with Negative above Positive by default.
- In
Subtractivemode, simulation layers subtract from the current composited simulation pixel. With passthrough off, subtractive-only stacks start from a white baseline (subtractive identity). For additive+subtractive-only mixed stacks, composition now uses a neutral gray baseline (50% normalization bias) so both layers remain visible instead of washing each other out; other mixed blend combinations keep black baseline. With passthrough on, subtraction uses the passthrough-backed pixel baseline. - Mixed Additive/Subtractive simulation stacks now accumulate in an order-neutral way (values clamp at the end of layer composition, not after each add/sub step), so swapping direct/inverse layer order does not bias the result.
- With passthrough enabled and compatible buffer dimensions, LifeViz now composites the source underlay into the render buffer first, then applies simulation layers in stack order on top. This preserves subtractive-layer detail inside bright passthrough regions (instead of washing out under additive underlay blending).
- Passthrough compositing now applies simulation as a delta over the underlay baseline, so enabling passthrough does not globally weaken life contrast.
- For additive/subtractive-only simulation stacks, passthrough now composes additive and subtractive families separately over the underlay, which prevents multi-layer collapse when both families are enabled.
- Capture buffers are pooled and only the downscaled composite is retained, eliminating GC spikes from per-frame allocations.
- Webcam sources stream via WinRT
MediaCapture; clearing sources or closing the app releases the camera. Cameras retry initialization once and wait longer for first frames before being removed. - Framerate lock: choose 15 / 30 / 60 / 144 fps from the context menu to match capture needs or ease CPU/GPU load. That menu now controls render/presentation cadence directly;
Level -> Frameratemodulates simulation speed against that target instead of throttling the whole app. - Startup is now guarded by a recovery flag. If the previous launch dies before the main window finishes initializing, the next launch falls back to a safe windowed startup (
<=480p,<=60 fps, fullscreen off,Show FPSoff,Level -> Framerateoff) instead of immediately reloading the bad persisted fullscreen/fps combination. - Show FPS now includes reactive diagnostics, separate
Render fpsandSim spsreadouts, liveSteps/frame, frame-pacing stats (avg,p95, and frame-gap spike counts), and a liveWork:breakdown (frame,composite,inject,sim,renderms averages over recent frames).Steps/frameis the actual catch-up count executed on the current render tick, so it immediately distinguishes "presentation is slow" from "simulation is jumping multiple generations per draw." It also draws two bottom-of-screen debug panels on a fixed 30-second window. The upper timing panel shows orange = rolling frame-gap history, red dashed = current frame budget, and amber vertical bars for individual frame-budget underruns/overruns so missed-present bursts are easy to spot. The lower audio panel shows white = rolling waveform bucket range (min/max per history slice), yellow = rolling rectified max-envelope history, cyan = rolling bass-energy history, magenta = rolling dominant-frequency history on a logarithmic musical-note scale, blue = bass-band dominant frequency, green = mid-band dominant frequency, and red = high-band dominant frequency. New data enters on the right and scrolls left. Yellow and cyan share the waveform zero baseline so you can compare fit directly, while the timing traces use a full-height0..50 msscale so pacing spikes are visible even when average simulation throughput still looks acceptable. Audio graph histories now come straight fromAudioBeatDetector's rolling history buffers instead of being re-sampled into UI-side queues, and the overlay is decimated to a capped sample count with split timing/audio refresh cadences so diagnostics stay readable without dominating visible rendering. - Capture threshold window: adjustable min/max sliders (with optional invert) in the context menu; this updates all simulation layers at once. Each simulation layer also has its own injection mode plus threshold min/max + invert controls in Scene Editor. In Threshold mode the window is a boolean gate (inside-window alive, or outside-window alive when inverted). In Random Pulse and Pulse Width modes the same per-layer window shapes injection intensity/probability (including invert), so min/max still strongly affect behavior.
- Injection noise: adjustable slider (0-1) that randomly skips cell injection per pixel to introduce controlled noise.
- RGB Channel Bins hue is now per simulation layer instead of global. Each layer exposes
Hue Offset, animatedHue Speed, and reactive mappings in the Scene Editor. AFrequency -> Hue Shiftmapping uses the same log-note-scaled dominant frequency shown by the magenta debug trace, so audio pitch can drive palette changes per layer. The same per-layer hue is used for RGB channel injection mapping and final RGB presentation. - Built-in recording: Start Recording writes to
%UserProfile%\\Videos\\LifeVizusing a pixel-perfect integer upscale to the nearest HD height (720/1080/1440/2160 when divisible). Use Recording Quality to pick Lossless (FFV1 in MKV, compressed), Crisp (H.264 in MP4, Windows Media Player compatible), Uncompressed (AVI, huge files), or H.264 tiers (High/Balanced/Compact); encoding favors quality-based VBR with bitrate caps so pixel lines stay crisp, and a taskbar overlay appears while active. If Audio Source is set, recording captures that selected input/output source in parallel and muxes it into the finished file on stop; audio muxing usesffmpegon PATH. The recorder preserves constant-FPS wall-clock duration by filling missed recording intervals with duplicate frames, applying encoder backpressure instead of dropping queued frames, and padding the final frame on stop when needed.
AutoClip video lists and timing ranges persist in the app config and in version 8 .lifevizlayers.json scene projects. During fixed-duration rendering, clip and delay decisions advance from the virtual output-frame clock; after export, the pre-render live clip/delay position is restored.
-
Life Simnow has a thirdLife Mode,Bitwise RGB. It injects the source image as 24 RGB bit planes (8 red, 8 green, 8 blue), evolves those planes independently with Conway rules, and reconstructs visible color from the current live bit states. This mode always uses an effective depth of24, so the project depth slider does not change its plane count. -
Bitwise RGBuses the same per-layer hue controls as the existing RGB-capable life path, soHue Offset,Hue Speed, and reactiveHue Shiftall apply to both injection mapping and final RGB presentation. -
Settings persist to
%AppData%\lifeviz\config.jsonafter the app finishes loading (height/rows, depth, framerate, blend/composite toggles, full simulation-layer stack settings including per-layer thresholds, opacity, per-layer RGB hue offset/speed, per-layer reactive mapping lists, passthrough, audio reactivity controls, etc.) and restore on next launch. Rapid slider/editor changes are coalesced for 500 ms, identical JSON is skipped, the final write runs off the UI thread through a same-directory temporary file, and shutdown drains the last pending revision. The window keeps the current aspect ratio and can be resized from the corner grip; use height presets or fullscreen to change simulation resolution without letterboxing. -
The source stack is restored too: window sources are matched by title, webcams by device id/name, file sources by path (video sequences restore their ordered path list), keeping order (including nested layer groups) plus per-layer blend mode, fit mode, opacity, video-audio toggle, video-audio volume, mirror, and keying settings (enable/color/range) when the inputs are available. The global source-audio master toggle/volume are also restored.
-
YouTube sources resolve asynchronously on launch so the UI can come up even if the stream lookup is slow; the layer starts once the stream URL is resolved (check logs for failures).
-
Aspect ratio lock state persists (default lock ratio is 16:9).
-
Fullscreen preference is remembered and re-applied on launch using the active monitor bounds so the taskbar stays hidden.
LifeViz still uses the ClickOnce publish target to generate versioned Windows payloads, but the installer now creates normal shortcuts to the staged app exe so Start Menu launches do not run ClickOnce update/install UI. Packaging requires the full .NET Framework MSBuild that ships with Visual Studio/Build Tools:
# Developer PowerShell where msbuild.exe is available
msbuild lifeviz.csproj `
/t:Publish `
/p:PublishProfile=Properties\PublishProfiles\WinClickOnce.pubxml `
/p:Configuration=ReleaseThe repo now bundles three helper scripts:
Publish-Installer.ps1- resolvesMSBuild.exe, removes the previous publish payload, then runs the publish target and writes only the current manifests + installer assets intobin/Release/net9.0-windows/publish/. Release callers pass-ProductVersionso assembly/file metadata matches the semantic tag; ordinary local builds remain explicitly markeddev.deploy.ps1- builds, publishes with an auto-generated version, then stages the fresh payload throughInstall-ClickOnce.ps1and refreshes the normalLifeVizshortcuts. Pass-RegisterClickOnceonly when you explicitly want ClickOnce app registration.Publish-GitHubRelease.ps1- builds, stamps the app, bootstrapper, and ClickOnce manifests from the selected semantic tag, publishes a ClickOnce payload, bundles it into a singlelifeviz_installer.exe(self-extracting + auto-install), and creates a GitHub release that uploads that one exe (requiresghCLI authenticated to your repo). The bootstrapper waits for installation to finish, records%TEMP%\lifeviz-install.log, and shows a real error if staging fails instead of exiting silently. It asks a quick vibe check (tiny tweak / glow-up / new era) and auto-bumps the version/tag for you-no need to invent numbers.Install-ClickOnce.ps1- bundled alongside published payloads; copies and validates the new payload in a sibling transaction directory, retries transient copy failures, waits for any running LifeViz process from the staged install to exit, then swaps the validated payload into%LOCALAPPDATA%\lifeviz-clickonce. The old install is left intact if pre-swap staging fails. It also removes stale.appref-msentries, creates directLifeVizStart Menu/Desktop shortcuts, and launches the staged exe. Pass-RegisterClickOnceif you need the legacy appref flow.- Rider users: run the lifeviz: Publish Installer (MSBuild.exe) configuration (stored in
.run/), which shells out toPublish-Installer.ps1so full MSBuild is used. The auto-generated Rider publish config usesdotnet msbuildand will fail withMSB4803. - If you do publish via
dotnet msbuild(e.g., Rider's generated publish config), the project automatically disables the ClickOnce bootstrapper so the build succeeds; use the MSBuild.exe-backed scripts to produce the optionalsetup.exebootstrapper.
Artifacts:
Application Files/lifeviz_<version>/...- versioned payload.lifeviz.application- ClickOnce manifest used as publish metadata;Install-ClickOnce.ps1resolves the currentApplication Files/lifeviz_<version>/lifeviz.exefrom it for direct shortcuts.setup.exe- optional bootstrapper for clean machines (installs prerequisites + shortcuts). Only needed for first-time installs.
To push a Windows release to GitHub:
gh auth login # one-time
.\Publish-GitHubRelease.ps1 -NotesPath release-notes.mdUse -Draft to stage without publishing. The script reuses Publish-Installer.ps1 to generate assets, stamps the chosen tag into the executable's product/file version and ClickOnce deployment version, and emits a single lifeviz_installer.exe into artifacts/github-release/; the GitHub release uploads just that executable.
NOTE:
.NET CLIalone cannot produce ClickOnce installers (MSB4803). Always use the full MSBuild toolchain, either directly (msbuild) or through the scripts above.
All technical details (rendering pipeline, controls, install flow) live in /wiki. Update the relevant page whenever you change the app; see agents.md for documentation expectations.