Cinematic screen effects and post-processing for Minecraft: 60+ effects, camera shake, overlays and a node editor in your browser. Client-side; works from commands or datapacks.
VFXWeaver puts a cinematic layer over Minecraft: 60+ effects rendered as post-processing shaders on top of the frame, camera moves, world and entity overlays, screen images and sound. Fire one from a command, build your own in a node editor that opens in your browser, or drive them from a datapack, a server plugin or your own mod.
Client-side only. Nothing to install on the server, no resource pack required.
Try it in two minutes
Drop vfxweaver-2.5.0+<minecraft>-<loader>.jar into mods/ (the versions tab filters by game version and loader).
Launch a world with cheats on, or be op on a server.
/vfx list prints every effect id. Then:
Command
What you see
/vfx play vfxweaver:datamosh
The picture melts: pixels of the old frame are dragged along the movement and the frame breaks into blocks of stale content.
/vfx play vfxweaver:torn_edge
The frame splits along a jagged line, the two sides pulled apart under a glowing edge.
/vfx play vfxweaver:spectral
A seven-stop prism ramp: a duotone, a chromatic smear or a spectral ring.
/vfx play vfxweaver:hex_grid
A triangular lattice whose lines take their colour from the screen position.
/vfx play vfxweaver:vhs
Tape wear: tracking bands, colour bleed, wobble.
/vfx play vfxweaver:heartbeat
A heartbeat in your ears, at a tempo you set.
/vfx play vfxweaver:vortex
The world spirals into the centre.
/vfx play vfxweaver:shockwave
A ring knocks the picture outward from a point.
/vfx play vfxweaver:camera_shake
An impact shake with a smooth envelope, not a jitter.
A play lasts a few seconds: run it again to repeat, /vfx stop <effect> to cut it. Any param is overridable in place, so /vfx play vfxweaver:vhs {[intensity:1.5]} is the same effect, harder. Several plays of the same effect stack instead of replacing each other.
Build your own in the browser
/vfxeditor starts a small node editor served by the game itself and opens it in your browser. The effect is a node graph; every param takes a constant, a start/end ramp, keyframes or an expression. The canvas plays live in-game while you drag, the timeline gives one lane per animated param, and Export writes an ordinary data/<namespace>/vfx/<name>.json — exactly what a datapack would contain, so there is no editor-only format and nothing extra to install.
The URL carries a one-time token, the bridge binds to 127.0.0.1 only, and closing the tab does not leave anything running.
Entity, 3 — tint, outline and displacement by UUID, texture-aware.
Audio, 4 — heartbeat, hum, noise and volume duck, synthesised on their own audio thread so the frame rate never reaches the sound.
Images and windows — screen_image draws a picture over the frame (a resource-pack PNG, animated or not). window_create opens a real frameless click-through desktop window showing a picture while the game runs.
Extras — fog modifier, masks (screen, world and entity volumes, down to custom GLSL plugins), collections (several effects as one), easing curves and keyframes everywhere.
Datapacks
A definition is one JSON file: an effect type, a timeline, params, and optional bindings to the world, the camera or the player. Params animate as a constant, a start/end ramp, keyframes with easing, or an expr, and can read the player's position, look direction or health.
The jar ships 73 ready definitions, one per effect type, so the fastest way to learn the format is to copy one out and change a number. Servers can fire effects over vfxweaver:vfx_trigger and sync definitions over vfxweaver:vfx_sync; packets carry a PROTOCOL_VERSION, and a client silently ignores an incompatible one instead of touching your world.
The datapack used to record the clips on the docs site is public: vfx-demos-showcase.zip. It builds a stage (/function vfx_demos:scene/setup) and plays one looping demo per effect (/vfx play vfx_demos:show_vhs), which is the quickest tour of everything here.
For mod developers
dev.vfxweaver.api.VFXAPI plays, stops, live-tunes and keyframes effects on a player from the server side, and registerImage lets your mod hand the renderer an image it drew itself. The network layout, the definition registry and the full call surface are in docs/API.md.
Requirements
Minecraft
26.2, 26.1.x, 1.21.11
Loader
Fabric 0.18.4+ on 26.x, 0.17.3+ on 1.21.11, or NeoForge
Fabric API
required on Fabric
Java
25+
Side
client; the server side is optional and only carries datapack sync and triggers
Effects recorded into a Flashback replay play back with it, including looping and persistent ones.
### Added
- **`hex_grid`: a triangular grid coloured by the screen position.** A new screen effect draws a triangular lattice over the frame - three line directions (two at 60 degrees, one vertical) on a `cell_size` cell (46 px by default) anchored at the screen centre - and paints the lines with the prism ramp picked by the position (`prism7(x * 0.6 + y * 0.4 + time * speed)`, drifting at 0.5 per second): the lines at 28 % of that colour over near-black `(0.01, 0.008, 0.02)`, and the highlights of the picture (brighter than `core`, 0.68 by default - absolute, not local, so no outline appears at a brightness step) in white; `amount` cross-fades the whole look (default `1.0`, neutral `0.0` so `fade_ticks` fades it) and no param has an upper cap - only the lower guards that avoid degenerate maths (`cell_size >= 1` px, `width >= 0.0001`). Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged. Documented in the [guide](guide/effects/screen/hex-grid.md). Asserted by `scripts/check-vfx-schema.ps1`, `scripts/check-shader-identifiers.ps1` and `scripts/check-builtins-parse.ps1`; the GLSL/pixels cannot be verified headless here. **Not verified in game**.
- **`spectral`: a rainbow prism look over the frame.** A new screen effect draws the picture through a seven-stop prism ramp - red `(1.0, 0.07, 0.1)`, orange `(1.0, 0.42, 0.03)`, yellow `(1.0, 0.88, 0.06)`, green `(0.1, 0.95, 0.28)`, blue `(0.06, 0.48, 1.0)`, violet `(0.28, 0.14, 1.0)`, magenta `(0.74, 0.16, 1.0)` - swept by the angle around `center_x`/`center_y`, the distance from it (0.9 per screen height) and the clock (0.7 per second); `mode` picks the variant: **0** the prism duotone (the frame is split into a dark and a bright half by its own brightness, `mid` 0.5 by default - absolute, not local, so no outline appears at a brightness step; the bright half takes the prism colour and the highlights, `core` 0.68, go white), **1** the chromatic smear (seven taps along the outward direction, spread by `0.0035 + 0.004 * distance`, every tap's edge `smoothstep(0.02, 0.30)` painted with its own prism stop - a wide ramp, so it stays a smear and not a drawn outline) and **2** the spectral ring (the luminance picks a band `floor(luma * 6.999)`, the distance picks a ring `floor(distance * 9 - time * 6)`, the two together pick a spectral stop and the highlights (`core` 0.68) go white; `amount` cross-fades the whole look (default `1.0`, neutral `0.0` so `fade_ticks` fades it), `mode` is an enum clamped to 0..2 and no other param has an upper cap. Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged. Documented in the [guide](guide/effects/screen/spectral.md). Asserted by `scripts/check-vfx-schema.ps1`, `scripts/check-shader-identifiers.ps1` and `scripts/check-builtins-parse.ps1`; the GLSL/pixels cannot be verified headless here. **Not verified in game**.
- **`torn_edge`: a ragged tear with a glowing line down it.** A new screen effect cuts the frame along a straight line through `center_x`/`center_y` whose angle is re-rolled every beat (`speed`, 20 per second by default): a value-noise ripple of `ragged` (0.02 of the screen height, 38 cycles across the tear) makes the edge wander, the two sides of the cut are drawn at opposite brightness (a pixel brighter than `mid`, 0.5 by default - the split is absolute, not local, so no outline appears at a brightness step), and the tear itself is a white line (an exponential falloff of `glow`, 0.0035 of the screen height, scaled by 1.6); `amount` cross-fades the whole look (default `1.0`, neutral `0.0` so `fade_ticks` fades it), and no param has an upper cap - only the lower guards that avoid degenerate maths (`speed >= 0`, `glow >= 0.0005`). Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged. Documented in the [guide](guide/effects/screen/torn-edge.md). Asserted by `scripts/check-vfx-schema.ps1`, `scripts/check-shader-identifiers.ps1` and `scripts/check-builtins-parse.ps1`; the GLSL/pixels cannot be verified headless here. **Not verified in game**.
- **`datamosh`: the frame is decoded against a reference that never arrives, and the movement search runs once per cell instead of once per pixel.** A new screen effect is a *feedback* effect: it keeps the picture it produced last frame in the frame's history buffer, and that history is the reference the decoder gets instead of a key frame. **The key frame never arrives**: the history only refreshes through the per-piece I-frame below, so the decoder keeps painting the screen as it was half a second ago. The movement is estimated the way a codec searches its reference frame for the matching block: one Lucas-Kanade step between the live picture and the reference (the reference carries the previous live frame's luma in its alpha) gives the velocity of the content, and a greedy search over 8, 4, 2 and 1 pixel finds the integer vector that best matches it, with a penalty that gives exactly zero where nothing moves. The search used to run per pixel (about 174 texture reads per pixel, a full-resolution pass the frame rate could not afford): every pixel of a piece needs the same vector, so the search now runs in its own pass at 1/8 resolution - one texel per 8x8 cell - and the update pass reads the result back. The picture is unchanged, the cost is about 6.5 reads per pixel instead of 174. The decoder then carries the pixels over as they are (point sampled, no filter, no leftover of the live frame), so a moving object drags sharp pixels of the old background with it; the luma is taken at the measured vector and the colour at `1 + chroma` times it, the way a 4:2:0 stream smears colour when it is decoded against the wrong reference - every colour is the picture's own, there is no random noise. The pieces come from a static hash quadtree (8 to 64 px), so neighbouring pieces differ in size and the layout never changes, and each piece returns to the real picture on its own phase every `iframe` seconds, so there is no global flash. `smear` (1) is the vector multiplier and `chroma` (0.5) the colour pull; only the lower guards exist (`chroma` and `amount` clamped to 0..1, `smear` and `iframe` not negative), no param has an upper cap. Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged. Honest limits, stated on the page: the melt lives in the same history buffer as `afterimage`, so the two chain when both run and each overwrites the alpha the other needs; the reference is the screen's own past rather than another shot; the movement needs some structure in the picture, so in flat areas (sky, a plain wall) nothing is dragged; and the motion field is one texel per 8x8 cell, so a slower movement than 8 px is not resolved. Documented in the [guide](guide/effects/screen/datamosh.md). Asserted by `scripts/check-vfx-schema.ps1`, `scripts/check-shader-identifiers.ps1`, `scripts/check-post-fusion.ps1` and `scripts/check-builtins-parse.ps1`; the GLSL/pixels cannot be verified headless here. **Not verified in game**.
- **`heartbeat`: a heartbeat positioned in the world.** A new audio effect plays a recording of one lub-dub that ships inside the mod (`rate` beat rate in bpm, default `60`; `volume` linear gain `0..1`, neutral `0.0` so `fade_ticks` fades it; `pitch` playback-speed multiplier, default `1` = as recorded) straight into the OS mixer — no resource pack needed, identical on every Minecraft line. Every beat restarts the recording, and the recording is time-scaled to the tempo: at 60 bpm it plays as recorded (0.85 s in a 1 s beat) and at 140 bpm it is compressed to about 0.36 s in a 0.43 s beat, with the pause inside the lub-dub closing up in the same proportion. The scaling keeps the tone where it was, so a fast heart is a fast recording on the same pitch rather than a higher one; `pitch` is a separate tone multiplier that does move the pitch; the pause between beats is whatever the recording leaves free, not a separate re-timing. A cut playback is faded over 5 ms so a restart never clicks, and `rate` and `pitch` are read fresh every tick, so keyframes/`expr`/bindings ride them live and the phase integrates per sample, so a rate change glides. Positioning, distance falloff (full within 8 blocks, silent past 48), clock semantics (pause freezes, seek resyncs silently), fade-out on single stop and immediate `stopAll`/world exit/client shutdown close all mirror the `hum`/`noise` voices; the same honest limits apply (vanilla mixer bypassed, no-audio-device stays silent with a one-time warning). Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged, no new dependency. Documented in the [guide](guide/effects/audio/heartbeat.md). Asserted by `scripts/check-vfx-schema.ps1` and `scripts/check-builtins-parse.ps1`; audible output cannot be verified headless here. **Not verified in game**.
- **`volume_duck`: scale a vanilla sound category down while active.** A new global (non-positional) audio effect sets one game-options category level to its pre-duck level times `volume` (`0..1`, neutral `1.0` so `fade_ticks` fades the duck in and out), selected by the `channel` param as a case-insensitive name (`master`, `music`, `records`, `weather`, `blocks`, `hostile`, `neutral`, `players`, `ambient`, `voice`, `ui`) or the historical `SoundSource` ordinal (`0..10`, back-compat); an unknown name warns once and safely no-ops instead of ducking the wrong category. Each frame only ducked channels are written (base x last-writer-wins scale across stacked ducks); the captured level is restored the frame a channel's last duck ends and on single stop, replay seek, `stopAll`, world exit and client shutdown, and nothing is ever written to `options.txt`. Honest limits, stated on the page: the options slider shows the scaled level and a mid-duck slider drag on that channel is overwritten, then lost on restore. Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged, no new dependency. Documented in the [guide](guide/effects/audio/volume-duck.md). Asserted by `scripts/check-vfx-schema.ps1` and `scripts/check-builtins-parse.ps1`; audible output cannot be verified headless here. **Not verified in game**.
- **`noise`: generated white noise positioned in the world.** A new audio effect synthesises mono white noise per effect instance (`volume` linear gain `0..1`, neutral `0.0` so `fade_ticks` fades it; `brightness` crisp/soft blend `0..1`, default `0.5`; `jitter` random-walk step with `jitter_rate` speed multiplier for a playback-rate and volume wander; `seed` reseeds the texture) straight into the OS mixer — no audio file in any pack, identical on every Minecraft line. Positioning, distance falloff (full within 8 blocks, silent past 48), clock semantics (pause freezes, seek resyncs silently), fade-out on single stop and immediate `stopAll`/world exit/client shutdown close all mirror the `hum` voice; the same honest limits apply (vanilla mixer bypassed, no-audio-device stays silent with a one-time warning). Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged, no new dependency. Documented in the [guide](guide/effects/audio/noise.md). Asserted by `scripts/check-vfx-schema.ps1` and `scripts/check-builtins-parse.ps1`; audible output cannot be verified headless here. **Not verified in game**.
- **`hum`: a generated sine hum positioned in the world.** A new audio effect synthesises a mono sine tone per effect instance (`frequency` in Hz, default `440`; `volume` linear gain `0..1`, neutral `0.0` so `fade_ticks` fades it) straight into the OS mixer — no audio file in any pack, identical on every Minecraft line. The voice is positioned like a world overlay (move position, first static `positions` entry, first entity anchor, authored `pos_x/y/z`, else the local-player snapshot) with a linear distance falloff (full within 8 blocks, silent past 48); it follows the shared effect clock, so pause freezes it and a replay seek resyncs silently. Stopping one instance fades it out; `stopAll`/world exit/client shutdown closes the voices at once. Honest limits, stated on the page: the vanilla mixer is bypassed (volume sliders do not apply — use `volume`), and a machine with no audio device stays silent with a one-time warning. Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged, no new dependency. Documented in the [guide](guide/effects/audio/hum.md). Asserted by `scripts/check-vfx-schema.ps1` and `scripts/check-builtins-parse.ps1`; audible output cannot be verified headless here. **Not verified in game**.
### Fixed
- **The frame rate no longer reaches the sound: the voices moved to their own audio thread.** The `hum`, `noise` and `heartbeat` voices were synthesised on the render thread, one chunk per frame, so the frame rate leaked into the sound: the gain ramps were one frame long, a long frame could starve the line's buffer (that is the crackling), and the heartbeat's time-scale work spiked the frame. The voices now live on a dedicated audio thread that generates fixed 5 ms blocks from its own wall clock and keeps every line about 60 ms ahead of its playhead, so a slow frame, a lost frame or an unfocused window cannot starve a line or stretch a ramp - the sound is the same at 30 fps and at 200 fps. The render thread only publishes the parameters and the shared effect clock; the voice state belongs to the audio thread, so there is nothing to lock. A paused game (or a paused replay) is still the one thing that stops it: the shared effect clock freezes and the thread renders silence until it moves again. The heartbeat's time-scale work moved to the audio thread with the rest. No param renamed, no wire, UBO or API change, `PROTOCOL_VERSION` unchanged. Documented on the [hum](guide/effects/audio/hum.md), [noise](guide/effects/audio/noise.md) and [heartbeat](guide/effects/audio/heartbeat.md) pages. **Not verified in game**.
- **The synthesised voices no longer crackle, and a heartbeat plays out instead of being cut.** Three click sources are gone. A voice that resumes after a silence gap (a pause, a game hitch, a fade) now fades in over 5 ms instead of starting at full amplitude. A voice whose effect ended is no longer cut at the playhead: the `hum` and the `noise` fade out and their line is released once it has drained, so the stop is a fade and not a step. And the last 5 ms of a heartbeat recording fade out, so a beat ends without a click. The heartbeat's `duration` now decides when the sound stops *repeating*: when the instance ends, the thump stops repeating right there and the beat that is sounding plays to its end instead of being cut in the middle. No param renamed, no wire, UBO or API change, `PROTOCOL_VERSION` unchanged. Documented on the [heartbeat](guide/effects/audio/heartbeat.md) page. **Not verified in game**.
- **Synthesised voices no longer stutter or crackle on slow frames.** The `hum`/`noise`/`heartbeat` voices counted samples from frame ticks, so an unfocused window (a few frames per second) pushed each frame past the 4-tick cap and the voice emitted nothing (chunk-gap-chunk), and the 0.25 s line buffer underran whenever the game loop stalled. Each voice now counts samples on its own steady wall clock gated by the effect clock: slow frames and replay seeks emit the wall-clock debt sized to the line's free buffer space (the write never blocks the frame), phases stay continuous so there are no bursts or boundary clicks, and pause (or a paused replay) still freezes synthesis exactly as before. The buffer grows to 0.5 s. No param renamed, no UBO or wire change, `PROTOCOL_VERSION` unchanged. Documented on the [hum](guide/effects/audio/hum.md), [noise](guide/effects/audio/noise.md) and [heartbeat](guide/effects/audio/heartbeat.md) pages; audible output cannot be verified headless here. **Not verified in game**.
- **Local-contrast halos around geometry in `halftone`, `comic` and `accent_glow` — local averaging removed.** The 8-tap averaging ring made the tone mapping a de-facto edge detector: near any luminance step the local mean skewed and the tone saturated into flat paper-coloured bands with no dots, which reads as glowing outlines and is useless for halftones. All three effects now map absolute pixel luminance instead (`halftone` keeps its adjustable `gain` contrast around mid grey, default `2.5` = neutral; `comic` gains a tunable `mid` split point, default `0.5`; `accent_glow` gains tunable `mid`/`core` thresholds, defaults `0.5`/`0.68`), so halos cannot form by construction — plus 8 fewer texture fetches per pixel. Dot grids grow from the screen centre instead of a corner, so changing `cell_size` rescales around the middle of the frame. Three new optional params (`mid` on `comic`; `mid`, `core` on `accent_glow`), no UBO or wire change, `PROTOCOL_VERSION` unchanged. **Not verified in game**.
### Added
- **`hex_grid`: a triangular grid coloured by the screen position.** A new screen effect draws a triangular lattice over the frame - three line directions (two at 60 degrees, one vertical) on a `cell_size` cell (46 px by default) anchored at the screen centre - and paints the lines with the prism ramp picked by the position (`prism7(x * 0.6 + y * 0.4 + time * speed)`, drifting at 0.5 per second): the lines at 28 % of that colour over near-black `(0.01, 0.008, 0.02)`, and the highlights of the picture (brighter than `core`, 0.68 by default - absolute, not local, so no outline appears at a brightness step) in white; `amount` cross-fades the whole look (default `1.0`, neutral `0.0` so `fade_ticks` fades it) and no param has an upper cap - only the lower guards that avoid degenerate maths (`cell_size >= 1` px, `width >= 0.0001`). Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged. Documented in the [guide](guide/effects/screen/hex-grid.md). Asserted by `scripts/check-vfx-schema.ps1`, `scripts/check-shader-identifiers.ps1` and `scripts/check-builtins-parse.ps1`; the GLSL/pixels cannot be verified headless here. **Not verified in game**.
- **`spectral`: a rainbow prism look over the frame.** A new screen effect draws the picture through a seven-stop prism ramp - red `(1.0, 0.07, 0.1)`, orange `(1.0, 0.42, 0.03)`, yellow `(1.0, 0.88, 0.06)`, green `(0.1, 0.95, 0.28)`, blue `(0.06, 0.48, 1.0)`, violet `(0.28, 0.14, 1.0)`, magenta `(0.74, 0.16, 1.0)` - swept by the angle around `center_x`/`center_y`, the distance from it (0.9 per screen height) and the clock (0.7 per second); `mode` picks the variant: **0** the prism duotone (the frame is split into a dark and a bright half by its own brightness, `mid` 0.5 by default - absolute, not local, so no outline appears at a brightness step; the bright half takes the prism colour and the highlights, `core` 0.68, go white), **1** the chromatic smear (seven taps along the outward direction, spread by `0.0035 + 0.004 * distance`, every tap's edge `smoothstep(0.02, 0.30)` painted with its own prism stop - a wide ramp, so it stays a smear and not a drawn outline) and **2** the spectral ring (the luminance picks a band `floor(luma * 6.999)`, the distance picks a ring `floor(distance * 9 - time * 6)`, the two together pick a spectral stop and the highlights (`core` 0.68) go white; `amount` cross-fades the whole look (default `1.0`, neutral `0.0` so `fade_ticks` fades it), `mode` is an enum clamped to 0..2 and no other param has an upper cap. Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged. Documented in the [guide](guide/effects/screen/spectral.md). Asserted by `scripts/check-vfx-schema.ps1`, `scripts/check-shader-identifiers.ps1` and `scripts/check-builtins-parse.ps1`; the GLSL/pixels cannot be verified headless here. **Not verified in game**.
- **`torn_edge`: a ragged tear with a glowing line down it.** A new screen effect cuts the frame along a straight line through `center_x`/`center_y` whose angle is re-rolled every beat (`speed`, 20 per second by default): a value-noise ripple of `ragged` (0.02 of the screen height, 38 cycles across the tear) makes the edge wander, the two sides of the cut are drawn at opposite brightness (a pixel brighter than `mid`, 0.5 by default - the split is absolute, not local, so no outline appears at a brightness step), and the tear itself is a white line (an exponential falloff of `glow`, 0.0035 of the screen height, scaled by 1.6); `amount` cross-fades the whole look (default `1.0`, neutral `0.0` so `fade_ticks` fades it), and no param has an upper cap - only the lower guards that avoid degenerate maths (`speed >= 0`, `glow >= 0.0005`). Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged. Documented in the [guide](guide/effects/screen/torn-edge.md). Asserted by `scripts/check-vfx-schema.ps1`, `scripts/check-shader-identifiers.ps1` and `scripts/check-builtins-parse.ps1`; the GLSL/pixels cannot be verified headless here. **Not verified in game**.
- **`datamosh`: the frame is decoded against a reference that never arrives, and the movement search runs once per cell instead of once per pixel.** A new screen effect is a *feedback* effect: it keeps the picture it produced last frame in the frame's history buffer, and that history is the reference the decoder gets instead of a key frame. **The key frame never arrives**: the history only refreshes through the per-piece I-frame below, so the decoder keeps painting the screen as it was half a second ago. The movement is estimated the way a codec searches its reference frame for the matching block: one Lucas-Kanade step between the live picture and the reference (the reference carries the previous live frame's luma in its alpha) gives the velocity of the content, and a greedy search over 8, 4, 2 and 1 pixel finds the integer vector that best matches it, with a penalty that gives exactly zero where nothing moves. The search used to run per pixel (about 174 texture reads per pixel, a full-resolution pass the frame rate could not afford): every pixel of a piece needs the same vector, so the search now runs in its own pass at 1/8 resolution - one texel per 8x8 cell - and the update pass reads the result back. The picture is unchanged, the cost is about 6.5 reads per pixel instead of 174. The decoder then carries the pixels over as they are (point sampled, no filter, no leftover of the live frame), so a moving object drags sharp pixels of the old background with it; the luma is taken at the measured vector and the colour at `1 + chroma` times it, the way a 4:2:0 stream smears colour when it is decoded against the wrong reference - every colour is the picture's own, there is no random noise. The pieces come from a static hash quadtree (8 to 64 px), so neighbouring pieces differ in size and the layout never changes, and each piece returns to the real picture on its own phase every `iframe` seconds, so there is no global flash. `smear` (1) is the vector multiplier and `chroma` (0.5) the colour pull; only the lower guards exist (`chroma` and `amount` clamped to 0..1, `smear` and `iframe` not negative), no param has an upper cap. Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged. Honest limits, stated on the page: the melt lives in the same history buffer as `afterimage`, so the two chain when both run and each overwrites the alpha the other needs; the reference is the screen's own past rather than another shot; the movement needs some structure in the picture, so in flat areas (sky, a plain wall) nothing is dragged; and the motion field is one texel per 8x8 cell, so a slower movement than 8 px is not resolved. Documented in the [guide](guide/effects/screen/datamosh.md). Asserted by `scripts/check-vfx-schema.ps1`, `scripts/check-shader-identifiers.ps1`, `scripts/check-post-fusion.ps1` and `scripts/check-builtins-parse.ps1`; the GLSL/pixels cannot be verified headless here. **Not verified in game**.
- **`heartbeat`: a heartbeat positioned in the world.** A new audio effect plays a recording of one lub-dub that ships inside the mod (`rate` beat rate in bpm, default `60`; `volume` linear gain `0..1`, neutral `0.0` so `fade_ticks` fades it; `pitch` playback-speed multiplier, default `1` = as recorded) straight into the OS mixer — no resource pack needed, identical on every Minecraft line. Every beat restarts the recording, and the recording is time-scaled to the tempo: at 60 bpm it plays as recorded (0.85 s in a 1 s beat) and at 140 bpm it is compressed to about 0.36 s in a 0.43 s beat, with the pause inside the lub-dub closing up in the same proportion. The scaling keeps the tone where it was, so a fast heart is a fast recording on the same pitch rather than a higher one; `pitch` is a separate tone multiplier that does move the pitch; the pause between beats is whatever the recording leaves free, not a separate re-timing. A cut playback is faded over 5 ms so a restart never clicks, and `rate` and `pitch` are read fresh every tick, so keyframes/`expr`/bindings ride them live and the phase integrates per sample, so a rate change glides. Positioning, distance falloff (full within 8 blocks, silent past 48), clock semantics (pause freezes, seek resyncs silently), fade-out on single stop and immediate `stopAll`/world exit/client shutdown close all mirror the `hum`/`noise` voices; the same honest limits apply (vanilla mixer bypassed, no-audio-device stays silent with a one-time warning). Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged, no new dependency. Documented in the [guide](guide/effects/audio/heartbeat.md). Asserted by `scripts/check-vfx-schema.ps1` and `scripts/check-builtins-parse.ps1`; audible output cannot be verified headless here. **Not verified in game**.
- **`volume_duck`: scale a vanilla sound category down while active.** A new global (non-positional) audio effect sets one game-options category level to its pre-duck level times `volume` (`0..1`, neutral `1.0` so `fade_ticks` fades the duck in and out), selected by the `channel` param as a case-insensitive name (`master`, `music`, `records`, `weather`, `blocks`, `hostile`, `neutral`, `players`, `ambient`, `voice`, `ui`) or the historical `SoundSource` ordinal (`0..10`, back-compat); an unknown name warns once and safely no-ops instead of ducking the wrong category. Each frame only ducked channels are written (base x last-writer-wins scale across stacked ducks); the captured level is restored the frame a channel's last duck ends and on single stop, replay seek, `stopAll`, world exit and client shutdown, and nothing is ever written to `options.txt`. Honest limits, stated on the page: the options slider shows the scaled level and a mid-duck slider drag on that channel is overwritten, then lost on restore. Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged, no new dependency. Documented in the [guide](guide/effects/audio/volume-duck.md). Asserted by `scripts/check-vfx-schema.ps1` and `scripts/check-builtins-parse.ps1`; audible output cannot be verified headless here. **Not verified in game**.
- **`noise`: generated white noise positioned in the world.** A new audio effect synthesises mono white noise per effect instance (`volume` linear gain `0..1`, neutral `0.0` so `fade_ticks` fades it; `brightness` crisp/soft blend `0..1`, default `0.5`; `jitter` random-walk step with `jitter_rate` speed multiplier for a playback-rate and volume wander; `seed` reseeds the texture) straight into the OS mixer — no audio file in any pack, identical on every Minecraft line. Positioning, distance falloff (full within 8 blocks, silent past 48), clock semantics (pause freezes, seek resyncs silently), fade-out on single stop and immediate `stopAll`/world exit/client shutdown close all mirror the `hum` voice; the same honest limits apply (vanilla mixer bypassed, no-audio-device stays silent with a one-time warning). Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged, no new dependency. Documented in the [guide](guide/effects/audio/noise.md). Asserted by `scripts/check-vfx-schema.ps1` and `scripts/check-builtins-parse.ps1`; audible output cannot be verified headless here. **Not verified in game**.
- **`hum`: a generated sine hum positioned in the world.** A new audio effect synthesises a mono sine tone per effect instance (`frequency` in Hz, default `440`; `volume` linear gain `0..1`, neutral `0.0` so `fade_ticks` fades it) straight into the OS mixer — no audio file in any pack, identical on every Minecraft line. The voice is positioned like a world overlay (move position, first static `positions` entry, first entity anchor, authored `pos_x/y/z`, else the local-player snapshot) with a linear distance falloff (full within 8 blocks, silent past 48); it follows the shared effect clock, so pause freezes it and a replay seek resyncs silently. Stopping one instance fades it out; `stopAll`/world exit/client shutdown closes the voices at once. Honest limits, stated on the page: the vanilla mixer is bypassed (volume sliders do not apply — use `volume`), and a machine with no audio device stays silent with a one-time warning. Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged, no new dependency. Documented in the [guide](guide/effects/audio/hum.md). Asserted by `scripts/check-vfx-schema.ps1` and `scripts/check-builtins-parse.ps1`; audible output cannot be verified headless here. **Not verified in game**.
### Fixed
- **The frame rate no longer reaches the sound: the voices moved to their own audio thread.** The `hum`, `noise` and `heartbeat` voices were synthesised on the render thread, one chunk per frame, so the frame rate leaked into the sound: the gain ramps were one frame long, a long frame could starve the line's buffer (that is the crackling), and the heartbeat's time-scale work spiked the frame. The voices now live on a dedicated audio thread that generates fixed 5 ms blocks from its own wall clock and keeps every line about 60 ms ahead of its playhead, so a slow frame, a lost frame or an unfocused window cannot starve a line or stretch a ramp - the sound is the same at 30 fps and at 200 fps. The render thread only publishes the parameters and the shared effect clock; the voice state belongs to the audio thread, so there is nothing to lock. A paused game (or a paused replay) is still the one thing that stops it: the shared effect clock freezes and the thread renders silence until it moves again. The heartbeat's time-scale work moved to the audio thread with the rest. No param renamed, no wire, UBO or API change, `PROTOCOL_VERSION` unchanged. Documented on the [hum](guide/effects/audio/hum.md), [noise](guide/effects/audio/noise.md) and [heartbeat](guide/effects/audio/heartbeat.md) pages. **Not verified in game**.
- **The synthesised voices no longer crackle, and a heartbeat plays out instead of being cut.** Three click sources are gone. A voice that resumes after a silence gap (a pause, a game hitch, a fade) now fades in over 5 ms instead of starting at full amplitude. A voice whose effect ended is no longer cut at the playhead: the `hum` and the `noise` fade out and their line is released once it has drained, so the stop is a fade and not a step. And the last 5 ms of a heartbeat recording fade out, so a beat ends without a click. The heartbeat's `duration` now decides when the sound stops *repeating*: when the instance ends, the thump stops repeating right there and the beat that is sounding plays to its end instead of being cut in the middle. No param renamed, no wire, UBO or API change, `PROTOCOL_VERSION` unchanged. Documented on the [heartbeat](guide/effects/audio/heartbeat.md) page. **Not verified in game**.
- **Synthesised voices no longer stutter or crackle on slow frames.** The `hum`/`noise`/`heartbeat` voices counted samples from frame ticks, so an unfocused window (a few frames per second) pushed each frame past the 4-tick cap and the voice emitted nothing (chunk-gap-chunk), and the 0.25 s line buffer underran whenever the game loop stalled. Each voice now counts samples on its own steady wall clock gated by the effect clock: slow frames and replay seeks emit the wall-clock debt sized to the line's free buffer space (the write never blocks the frame), phases stay continuous so there are no bursts or boundary clicks, and pause (or a paused replay) still freezes synthesis exactly as before. The buffer grows to 0.5 s. No param renamed, no UBO or wire change, `PROTOCOL_VERSION` unchanged. Documented on the [hum](guide/effects/audio/hum.md), [noise](guide/effects/audio/noise.md) and [heartbeat](guide/effects/audio/heartbeat.md) pages; audible output cannot be verified headless here. **Not verified in game**.
- **Local-contrast halos around geometry in `halftone`, `comic` and `accent_glow` — local averaging removed.** The 8-tap averaging ring made the tone mapping a de-facto edge detector: near any luminance step the local mean skewed and the tone saturated into flat paper-coloured bands with no dots, which reads as glowing outlines and is useless for halftones. All three effects now map absolute pixel luminance instead (`halftone` keeps its adjustable `gain` contrast around mid grey, default `2.5` = neutral; `comic` gains a tunable `mid` split point, default `0.5`; `accent_glow` gains tunable `mid`/`core` thresholds, defaults `0.5`/`0.68`), so halos cannot form by construction — plus 8 fewer texture fetches per pixel. Dot grids grow from the screen centre instead of a corner, so changing `cell_size` rescales around the middle of the frame. Three new optional params (`mid` on `comic`; `mid`, `core` on `accent_glow`), no UBO or wire change, `PROTOCOL_VERSION` unchanged. **Not verified in game**.
### Added
- **`hex_grid`: a triangular grid coloured by the screen position.** A new screen effect draws a triangular lattice over the frame - three line directions (two at 60 degrees, one vertical) on a `cell_size` cell (46 px by default) anchored at the screen centre - and paints the lines with the prism ramp picked by the position (`prism7(x * 0.6 + y * 0.4 + time * speed)`, drifting at 0.5 per second): the lines at 28 % of that colour over near-black `(0.01, 0.008, 0.02)`, and the highlights of the picture (brighter than `core`, 0.68 by default - absolute, not local, so no outline appears at a brightness step) in white; `amount` cross-fades the whole look (default `1.0`, neutral `0.0` so `fade_ticks` fades it) and no param has an upper cap - only the lower guards that avoid degenerate maths (`cell_size >= 1` px, `width >= 0.0001`). Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged. Documented in the [guide](guide/effects/screen/hex-grid.md). Asserted by `scripts/check-vfx-schema.ps1`, `scripts/check-shader-identifiers.ps1` and `scripts/check-builtins-parse.ps1`; the GLSL/pixels cannot be verified headless here. **Not verified in game**.
- **`spectral`: a rainbow prism look over the frame.** A new screen effect draws the picture through a seven-stop prism ramp - red `(1.0, 0.07, 0.1)`, orange `(1.0, 0.42, 0.03)`, yellow `(1.0, 0.88, 0.06)`, green `(0.1, 0.95, 0.28)`, blue `(0.06, 0.48, 1.0)`, violet `(0.28, 0.14, 1.0)`, magenta `(0.74, 0.16, 1.0)` - swept by the angle around `center_x`/`center_y`, the distance from it (0.9 per screen height) and the clock (0.7 per second); `mode` picks the variant: **0** the prism duotone (the frame is split into a dark and a bright half by its own brightness, `mid` 0.5 by default - absolute, not local, so no outline appears at a brightness step; the bright half takes the prism colour and the highlights, `core` 0.68, go white), **1** the chromatic smear (seven taps along the outward direction, spread by `0.0035 + 0.004 * distance`, every tap's edge `smoothstep(0.02, 0.30)` painted with its own prism stop - a wide ramp, so it stays a smear and not a drawn outline) and **2** the spectral ring (the luminance picks a band `floor(luma * 6.999)`, the distance picks a ring `floor(distance * 9 - time * 6)`, the two together pick a spectral stop and the highlights (`core` 0.68) go white; `amount` cross-fades the whole look (default `1.0`, neutral `0.0` so `fade_ticks` fades it), `mode` is an enum clamped to 0..2 and no other param has an upper cap. Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged. Documented in the [guide](guide/effects/screen/spectral.md). Asserted by `scripts/check-vfx-schema.ps1`, `scripts/check-shader-identifiers.ps1` and `scripts/check-builtins-parse.ps1`; the GLSL/pixels cannot be verified headless here. **Not verified in game**.
- **`torn_edge`: a ragged tear with a glowing line down it.** A new screen effect cuts the frame along a straight line through `center_x`/`center_y` whose angle is re-rolled every beat (`speed`, 20 per second by default): a value-noise ripple of `ragged` (0.02 of the screen height, 38 cycles across the tear) makes the edge wander, the two sides of the cut are drawn at opposite brightness (a pixel brighter than `mid`, 0.5 by default - the split is absolute, not local, so no outline appears at a brightness step), and the tear itself is a white line (an exponential falloff of `glow`, 0.0035 of the screen height, scaled by 1.6); `amount` cross-fades the whole look (default `1.0`, neutral `0.0` so `fade_ticks` fades it), and no param has an upper cap - only the lower guards that avoid degenerate maths (`speed >= 0`, `glow >= 0.0005`). Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged. Documented in the [guide](guide/effects/screen/torn-edge.md). Asserted by `scripts/check-vfx-schema.ps1`, `scripts/check-shader-identifiers.ps1` and `scripts/check-builtins-parse.ps1`; the GLSL/pixels cannot be verified headless here. **Not verified in game**.
- **`datamosh`: the frame is decoded against a reference that never arrives, and the movement search runs once per cell instead of once per pixel.** A new screen effect is a *feedback* effect: it keeps the picture it produced last frame in the frame's history buffer, and that history is the reference the decoder gets instead of a key frame. **The key frame never arrives**: the history only refreshes through the per-piece I-frame below, so the decoder keeps painting the screen as it was half a second ago. The movement is estimated the way a codec searches its reference frame for the matching block: one Lucas-Kanade step between the live picture and the reference (the reference carries the previous live frame's luma in its alpha) gives the velocity of the content, and a greedy search over 8, 4, 2 and 1 pixel finds the integer vector that best matches it, with a penalty that gives exactly zero where nothing moves. The search used to run per pixel (about 174 texture reads per pixel, a full-resolution pass the frame rate could not afford): every pixel of a piece needs the same vector, so the search now runs in its own pass at 1/8 resolution - one texel per 8x8 cell - and the update pass reads the result back. The picture is unchanged, the cost is about 6.5 reads per pixel instead of 174. The decoder then carries the pixels over as they are (point sampled, no filter, no leftover of the live frame), so a moving object drags sharp pixels of the old background with it; the luma is taken at the measured vector and the colour at `1 + chroma` times it, the way a 4:2:0 stream smears colour when it is decoded against the wrong reference - every colour is the picture's own, there is no random noise. The pieces come from a static hash quadtree (8 to 64 px), so neighbouring pieces differ in size and the layout never changes, and each piece returns to the real picture on its own phase every `iframe` seconds, so there is no global flash. `smear` (1) is the vector multiplier and `chroma` (0.5) the colour pull; only the lower guards exist (`chroma` and `amount` clamped to 0..1, `smear` and `iframe` not negative), no param has an upper cap. Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged. Honest limits, stated on the page: the melt lives in the same history buffer as `afterimage`, so the two chain when both run and each overwrites the alpha the other needs; the reference is the screen's own past rather than another shot; the movement needs some structure in the picture, so in flat areas (sky, a plain wall) nothing is dragged; and the motion field is one texel per 8x8 cell, so a slower movement than 8 px is not resolved. Documented in the [guide](guide/effects/screen/datamosh.md). Asserted by `scripts/check-vfx-schema.ps1`, `scripts/check-shader-identifiers.ps1`, `scripts/check-post-fusion.ps1` and `scripts/check-builtins-parse.ps1`; the GLSL/pixels cannot be verified headless here. **Not verified in game**.
- **`heartbeat`: a heartbeat positioned in the world.** A new audio effect plays a recording of one lub-dub that ships inside the mod (`rate` beat rate in bpm, default `60`; `volume` linear gain `0..1`, neutral `0.0` so `fade_ticks` fades it; `pitch` playback-speed multiplier, default `1` = as recorded) straight into the OS mixer — no resource pack needed, identical on every Minecraft line. Every beat restarts the recording, and the recording is time-scaled to the tempo: at 60 bpm it plays as recorded (0.85 s in a 1 s beat) and at 140 bpm it is compressed to about 0.36 s in a 0.43 s beat, with the pause inside the lub-dub closing up in the same proportion. The scaling keeps the tone where it was, so a fast heart is a fast recording on the same pitch rather than a higher one; `pitch` is a separate tone multiplier that does move the pitch; the pause between beats is whatever the recording leaves free, not a separate re-timing. A cut playback is faded over 5 ms so a restart never clicks, and `rate` and `pitch` are read fresh every tick, so keyframes/`expr`/bindings ride them live and the phase integrates per sample, so a rate change glides. Positioning, distance falloff (full within 8 blocks, silent past 48), clock semantics (pause freezes, seek resyncs silently), fade-out on single stop and immediate `stopAll`/world exit/client shutdown close all mirror the `hum`/`noise` voices; the same honest limits apply (vanilla mixer bypassed, no-audio-device stays silent with a one-time warning). Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged, no new dependency. Documented in the [guide](guide/effects/audio/heartbeat.md). Asserted by `scripts/check-vfx-schema.ps1` and `scripts/check-builtins-parse.ps1`; audible output cannot be verified headless here. **Not verified in game**.
- **`volume_duck`: scale a vanilla sound category down while active.** A new global (non-positional) audio effect sets one game-options category level to its pre-duck level times `volume` (`0..1`, neutral `1.0` so `fade_ticks` fades the duck in and out), selected by the `channel` param as a case-insensitive name (`master`, `music`, `records`, `weather`, `blocks`, `hostile`, `neutral`, `players`, `ambient`, `voice`, `ui`) or the historical `SoundSource` ordinal (`0..10`, back-compat); an unknown name warns once and safely no-ops instead of ducking the wrong category. Each frame only ducked channels are written (base x last-writer-wins scale across stacked ducks); the captured level is restored the frame a channel's last duck ends and on single stop, replay seek, `stopAll`, world exit and client shutdown, and nothing is ever written to `options.txt`. Honest limits, stated on the page: the options slider shows the scaled level and a mid-duck slider drag on that channel is overwritten, then lost on restore. Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged, no new dependency. Documented in the [guide](guide/effects/audio/volume-duck.md). Asserted by `scripts/check-vfx-schema.ps1` and `scripts/check-builtins-parse.ps1`; audible output cannot be verified headless here. **Not verified in game**.
- **`noise`: generated white noise positioned in the world.** A new audio effect synthesises mono white noise per effect instance (`volume` linear gain `0..1`, neutral `0.0` so `fade_ticks` fades it; `brightness` crisp/soft blend `0..1`, default `0.5`; `jitter` random-walk step with `jitter_rate` speed multiplier for a playback-rate and volume wander; `seed` reseeds the texture) straight into the OS mixer — no audio file in any pack, identical on every Minecraft line. Positioning, distance falloff (full within 8 blocks, silent past 48), clock semantics (pause freezes, seek resyncs silently), fade-out on single stop and immediate `stopAll`/world exit/client shutdown close all mirror the `hum` voice; the same honest limits apply (vanilla mixer bypassed, no-audio-device stays silent with a one-time warning). Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged, no new dependency. Documented in the [guide](guide/effects/audio/noise.md). Asserted by `scripts/check-vfx-schema.ps1` and `scripts/check-builtins-parse.ps1`; audible output cannot be verified headless here. **Not verified in game**.
- **`hum`: a generated sine hum positioned in the world.** A new audio effect synthesises a mono sine tone per effect instance (`frequency` in Hz, default `440`; `volume` linear gain `0..1`, neutral `0.0` so `fade_ticks` fades it) straight into the OS mixer — no audio file in any pack, identical on every Minecraft line. The voice is positioned like a world overlay (move position, first static `positions` entry, first entity anchor, authored `pos_x/y/z`, else the local-player snapshot) with a linear distance falloff (full within 8 blocks, silent past 48); it follows the shared effect clock, so pause freezes it and a replay seek resyncs silently. Stopping one instance fades it out; `stopAll`/world exit/client shutdown closes the voices at once. Honest limits, stated on the page: the vanilla mixer is bypassed (volume sliders do not apply — use `volume`), and a machine with no audio device stays silent with a one-time warning. Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged, no new dependency. Documented in the [guide](guide/effects/audio/hum.md). Asserted by `scripts/check-vfx-schema.ps1` and `scripts/check-builtins-parse.ps1`; audible output cannot be verified headless here. **Not verified in game**.
### Fixed
- **The frame rate no longer reaches the sound: the voices moved to their own audio thread.** The `hum`, `noise` and `heartbeat` voices were synthesised on the render thread, one chunk per frame, so the frame rate leaked into the sound: the gain ramps were one frame long, a long frame could starve the line's buffer (that is the crackling), and the heartbeat's time-scale work spiked the frame. The voices now live on a dedicated audio thread that generates fixed 5 ms blocks from its own wall clock and keeps every line about 60 ms ahead of its playhead, so a slow frame, a lost frame or an unfocused window cannot starve a line or stretch a ramp - the sound is the same at 30 fps and at 200 fps. The render thread only publishes the parameters and the shared effect clock; the voice state belongs to the audio thread, so there is nothing to lock. A paused game (or a paused replay) is still the one thing that stops it: the shared effect clock freezes and the thread renders silence until it moves again. The heartbeat's time-scale work moved to the audio thread with the rest. No param renamed, no wire, UBO or API change, `PROTOCOL_VERSION` unchanged. Documented on the [hum](guide/effects/audio/hum.md), [noise](guide/effects/audio/noise.md) and [heartbeat](guide/effects/audio/heartbeat.md) pages. **Not verified in game**.
- **The synthesised voices no longer crackle, and a heartbeat plays out instead of being cut.** Three click sources are gone. A voice that resumes after a silence gap (a pause, a game hitch, a fade) now fades in over 5 ms instead of starting at full amplitude. A voice whose effect ended is no longer cut at the playhead: the `hum` and the `noise` fade out and their line is released once it has drained, so the stop is a fade and not a step. And the last 5 ms of a heartbeat recording fade out, so a beat ends without a click. The heartbeat's `duration` now decides when the sound stops *repeating*: when the instance ends, the thump stops repeating right there and the beat that is sounding plays to its end instead of being cut in the middle. No param renamed, no wire, UBO or API change, `PROTOCOL_VERSION` unchanged. Documented on the [heartbeat](guide/effects/audio/heartbeat.md) page. **Not verified in game**.
- **Synthesised voices no longer stutter or crackle on slow frames.** The `hum`/`noise`/`heartbeat` voices counted samples from frame ticks, so an unfocused window (a few frames per second) pushed each frame past the 4-tick cap and the voice emitted nothing (chunk-gap-chunk), and the 0.25 s line buffer underran whenever the game loop stalled. Each voice now counts samples on its own steady wall clock gated by the effect clock: slow frames and replay seeks emit the wall-clock debt sized to the line's free buffer space (the write never blocks the frame), phases stay continuous so there are no bursts or boundary clicks, and pause (or a paused replay) still freezes synthesis exactly as before. The buffer grows to 0.5 s. No param renamed, no UBO or wire change, `PROTOCOL_VERSION` unchanged. Documented on the [hum](guide/effects/audio/hum.md), [noise](guide/effects/audio/noise.md) and [heartbeat](guide/effects/audio/heartbeat.md) pages; audible output cannot be verified headless here. **Not verified in game**.
- **Local-contrast halos around geometry in `halftone`, `comic` and `accent_glow` — local averaging removed.** The 8-tap averaging ring made the tone mapping a de-facto edge detector: near any luminance step the local mean skewed and the tone saturated into flat paper-coloured bands with no dots, which reads as glowing outlines and is useless for halftones. All three effects now map absolute pixel luminance instead (`halftone` keeps its adjustable `gain` contrast around mid grey, default `2.5` = neutral; `comic` gains a tunable `mid` split point, default `0.5`; `accent_glow` gains tunable `mid`/`core` thresholds, defaults `0.5`/`0.68`), so halos cannot form by construction — plus 8 fewer texture fetches per pixel. Dot grids grow from the screen centre instead of a corner, so changing `cell_size` rescales around the middle of the frame. Three new optional params (`mid` on `comic`; `mid`, `core` on `accent_glow`), no UBO or wire change, `PROTOCOL_VERSION` unchanged. **Not verified in game**.
### Added
- **`hex_grid`: a triangular grid coloured by the screen position.** A new screen effect draws a triangular lattice over the frame - three line directions (two at 60 degrees, one vertical) on a `cell_size` cell (46 px by default) anchored at the screen centre - and paints the lines with the prism ramp picked by the position (`prism7(x * 0.6 + y * 0.4 + time * speed)`, drifting at 0.5 per second): the lines at 28 % of that colour over near-black `(0.01, 0.008, 0.02)`, and the highlights of the picture (brighter than `core`, 0.68 by default - absolute, not local, so no outline appears at a brightness step) in white; `amount` cross-fades the whole look (default `1.0`, neutral `0.0` so `fade_ticks` fades it) and no param has an upper cap - only the lower guards that avoid degenerate maths (`cell_size >= 1` px, `width >= 0.0001`). Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged. Documented in the [guide](guide/effects/screen/hex-grid.md). Asserted by `scripts/check-vfx-schema.ps1`, `scripts/check-shader-identifiers.ps1` and `scripts/check-builtins-parse.ps1`; the GLSL/pixels cannot be verified headless here. **Not verified in game**.
- **`spectral`: a rainbow prism look over the frame.** A new screen effect draws the picture through a seven-stop prism ramp - red `(1.0, 0.07, 0.1)`, orange `(1.0, 0.42, 0.03)`, yellow `(1.0, 0.88, 0.06)`, green `(0.1, 0.95, 0.28)`, blue `(0.06, 0.48, 1.0)`, violet `(0.28, 0.14, 1.0)`, magenta `(0.74, 0.16, 1.0)` - swept by the angle around `center_x`/`center_y`, the distance from it (0.9 per screen height) and the clock (0.7 per second); `mode` picks the variant: **0** the prism duotone (the frame is split into a dark and a bright half by its own brightness, `mid` 0.5 by default - absolute, not local, so no outline appears at a brightness step; the bright half takes the prism colour and the highlights, `core` 0.68, go white), **1** the chromatic smear (seven taps along the outward direction, spread by `0.0035 + 0.004 * distance`, every tap's edge `smoothstep(0.02, 0.30)` painted with its own prism stop - a wide ramp, so it stays a smear and not a drawn outline) and **2** the spectral ring (the luminance picks a band `floor(luma * 6.999)`, the distance picks a ring `floor(distance * 9 - time * 6)`, the two together pick a spectral stop and the highlights (`core` 0.68) go white; `amount` cross-fades the whole look (default `1.0`, neutral `0.0` so `fade_ticks` fades it), `mode` is an enum clamped to 0..2 and no other param has an upper cap. Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged. Documented in the [guide](guide/effects/screen/spectral.md). Asserted by `scripts/check-vfx-schema.ps1`, `scripts/check-shader-identifiers.ps1` and `scripts/check-builtins-parse.ps1`; the GLSL/pixels cannot be verified headless here. **Not verified in game**.
- **`torn_edge`: a ragged tear with a glowing line down it.** A new screen effect cuts the frame along a straight line through `center_x`/`center_y` whose angle is re-rolled every beat (`speed`, 20 per second by default): a value-noise ripple of `ragged` (0.02 of the screen height, 38 cycles across the tear) makes the edge wander, the two sides of the cut are drawn at opposite brightness (a pixel brighter than `mid`, 0.5 by default - the split is absolute, not local, so no outline appears at a brightness step), and the tear itself is a white line (an exponential falloff of `glow`, 0.0035 of the screen height, scaled by 1.6); `amount` cross-fades the whole look (default `1.0`, neutral `0.0` so `fade_ticks` fades it), and no param has an upper cap - only the lower guards that avoid degenerate maths (`speed >= 0`, `glow >= 0.0005`). Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged. Documented in the [guide](guide/effects/screen/torn-edge.md). Asserted by `scripts/check-vfx-schema.ps1`, `scripts/check-shader-identifiers.ps1` and `scripts/check-builtins-parse.ps1`; the GLSL/pixels cannot be verified headless here. **Not verified in game**.
- **`datamosh`: the frame is decoded against a reference that never arrives, and the movement search runs once per cell instead of once per pixel.** A new screen effect is a *feedback* effect: it keeps the picture it produced last frame in the frame's history buffer, and that history is the reference the decoder gets instead of a key frame. **The key frame never arrives**: the history only refreshes through the per-piece I-frame below, so the decoder keeps painting the screen as it was half a second ago. The movement is estimated the way a codec searches its reference frame for the matching block: one Lucas-Kanade step between the live picture and the reference (the reference carries the previous live frame's luma in its alpha) gives the velocity of the content, and a greedy search over 8, 4, 2 and 1 pixel finds the integer vector that best matches it, with a penalty that gives exactly zero where nothing moves. The search used to run per pixel (about 174 texture reads per pixel, a full-resolution pass the frame rate could not afford): every pixel of a piece needs the same vector, so the search now runs in its own pass at 1/8 resolution - one texel per 8x8 cell - and the update pass reads the result back. The picture is unchanged, the cost is about 6.5 reads per pixel instead of 174. The decoder then carries the pixels over as they are (point sampled, no filter, no leftover of the live frame), so a moving object drags sharp pixels of the old background with it; the luma is taken at the measured vector and the colour at `1 + chroma` times it, the way a 4:2:0 stream smears colour when it is decoded against the wrong reference - every colour is the picture's own, there is no random noise. The pieces come from a static hash quadtree (8 to 64 px), so neighbouring pieces differ in size and the layout never changes, and each piece returns to the real picture on its own phase every `iframe` seconds, so there is no global flash. `smear` (1) is the vector multiplier and `chroma` (0.5) the colour pull; only the lower guards exist (`chroma` and `amount` clamped to 0..1, `smear` and `iframe` not negative), no param has an upper cap. Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged. Honest limits, stated on the page: the melt lives in the same history buffer as `afterimage`, so the two chain when both run and each overwrites the alpha the other needs; the reference is the screen's own past rather than another shot; the movement needs some structure in the picture, so in flat areas (sky, a plain wall) nothing is dragged; and the motion field is one texel per 8x8 cell, so a slower movement than 8 px is not resolved. Documented in the [guide](guide/effects/screen/datamosh.md). Asserted by `scripts/check-vfx-schema.ps1`, `scripts/check-shader-identifiers.ps1`, `scripts/check-post-fusion.ps1` and `scripts/check-builtins-parse.ps1`; the GLSL/pixels cannot be verified headless here. **Not verified in game**.
- **`heartbeat`: a heartbeat positioned in the world.** A new audio effect plays a recording of one lub-dub that ships inside the mod (`rate` beat rate in bpm, default `60`; `volume` linear gain `0..1`, neutral `0.0` so `fade_ticks` fades it; `pitch` playback-speed multiplier, default `1` = as recorded) straight into the OS mixer — no resource pack needed, identical on every Minecraft line. Every beat restarts the recording, and the recording is time-scaled to the tempo: at 60 bpm it plays as recorded (0.85 s in a 1 s beat) and at 140 bpm it is compressed to about 0.36 s in a 0.43 s beat, with the pause inside the lub-dub closing up in the same proportion. The scaling keeps the tone where it was, so a fast heart is a fast recording on the same pitch rather than a higher one; `pitch` is a separate tone multiplier that does move the pitch; the pause between beats is whatever the recording leaves free, not a separate re-timing. A cut playback is faded over 5 ms so a restart never clicks, and `rate` and `pitch` are read fresh every tick, so keyframes/`expr`/bindings ride them live and the phase integrates per sample, so a rate change glides. Positioning, distance falloff (full within 8 blocks, silent past 48), clock semantics (pause freezes, seek resyncs silently), fade-out on single stop and immediate `stopAll`/world exit/client shutdown close all mirror the `hum`/`noise` voices; the same honest limits apply (vanilla mixer bypassed, no-audio-device stays silent with a one-time warning). Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged, no new dependency. Documented in the [guide](guide/effects/audio/heartbeat.md). Asserted by `scripts/check-vfx-schema.ps1` and `scripts/check-builtins-parse.ps1`; audible output cannot be verified headless here. **Not verified in game**.
- **`volume_duck`: scale a vanilla sound category down while active.** A new global (non-positional) audio effect sets one game-options category level to its pre-duck level times `volume` (`0..1`, neutral `1.0` so `fade_ticks` fades the duck in and out), selected by the `channel` param as a case-insensitive name (`master`, `music`, `records`, `weather`, `blocks`, `hostile`, `neutral`, `players`, `ambient`, `voice`, `ui`) or the historical `SoundSource` ordinal (`0..10`, back-compat); an unknown name warns once and safely no-ops instead of ducking the wrong category. Each frame only ducked channels are written (base x last-writer-wins scale across stacked ducks); the captured level is restored the frame a channel's last duck ends and on single stop, replay seek, `stopAll`, world exit and client shutdown, and nothing is ever written to `options.txt`. Honest limits, stated on the page: the options slider shows the scaled level and a mid-duck slider drag on that channel is overwritten, then lost on restore. Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged, no new dependency. Documented in the [guide](guide/effects/audio/volume-duck.md). Asserted by `scripts/check-vfx-schema.ps1` and `scripts/check-builtins-parse.ps1`; audible output cannot be verified headless here. **Not verified in game**.
- **`noise`: generated white noise positioned in the world.** A new audio effect synthesises mono white noise per effect instance (`volume` linear gain `0..1`, neutral `0.0` so `fade_ticks` fades it; `brightness` crisp/soft blend `0..1`, default `0.5`; `jitter` random-walk step with `jitter_rate` speed multiplier for a playback-rate and volume wander; `seed` reseeds the texture) straight into the OS mixer — no audio file in any pack, identical on every Minecraft line. Positioning, distance falloff (full within 8 blocks, silent past 48), clock semantics (pause freezes, seek resyncs silently), fade-out on single stop and immediate `stopAll`/world exit/client shutdown close all mirror the `hum` voice; the same honest limits apply (vanilla mixer bypassed, no-audio-device stays silent with a one-time warning). Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged, no new dependency. Documented in the [guide](guide/effects/audio/noise.md). Asserted by `scripts/check-vfx-schema.ps1` and `scripts/check-builtins-parse.ps1`; audible output cannot be verified headless here. **Not verified in game**.
- **`hum`: a generated sine hum positioned in the world.** A new audio effect synthesises a mono sine tone per effect instance (`frequency` in Hz, default `440`; `volume` linear gain `0..1`, neutral `0.0` so `fade_ticks` fades it) straight into the OS mixer — no audio file in any pack, identical on every Minecraft line. The voice is positioned like a world overlay (move position, first static `positions` entry, first entity anchor, authored `pos_x/y/z`, else the local-player snapshot) with a linear distance falloff (full within 8 blocks, silent past 48); it follows the shared effect clock, so pause freezes it and a replay seek resyncs silently. Stopping one instance fades it out; `stopAll`/world exit/client shutdown closes the voices at once. Honest limits, stated on the page: the vanilla mixer is bypassed (volume sliders do not apply — use `volume`), and a machine with no audio device stays silent with a one-time warning. Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged, no new dependency. Documented in the [guide](guide/effects/audio/hum.md). Asserted by `scripts/check-vfx-schema.ps1` and `scripts/check-builtins-parse.ps1`; audible output cannot be verified headless here. **Not verified in game**.
### Fixed
- **The frame rate no longer reaches the sound: the voices moved to their own audio thread.** The `hum`, `noise` and `heartbeat` voices were synthesised on the render thread, one chunk per frame, so the frame rate leaked into the sound: the gain ramps were one frame long, a long frame could starve the line's buffer (that is the crackling), and the heartbeat's time-scale work spiked the frame. The voices now live on a dedicated audio thread that generates fixed 5 ms blocks from its own wall clock and keeps every line about 60 ms ahead of its playhead, so a slow frame, a lost frame or an unfocused window cannot starve a line or stretch a ramp - the sound is the same at 30 fps and at 200 fps. The render thread only publishes the parameters and the shared effect clock; the voice state belongs to the audio thread, so there is nothing to lock. A paused game (or a paused replay) is still the one thing that stops it: the shared effect clock freezes and the thread renders silence until it moves again. The heartbeat's time-scale work moved to the audio thread with the rest. No param renamed, no wire, UBO or API change, `PROTOCOL_VERSION` unchanged. Documented on the [hum](guide/effects/audio/hum.md), [noise](guide/effects/audio/noise.md) and [heartbeat](guide/effects/audio/heartbeat.md) pages. **Not verified in game**.
- **The synthesised voices no longer crackle, and a heartbeat plays out instead of being cut.** Three click sources are gone. A voice that resumes after a silence gap (a pause, a game hitch, a fade) now fades in over 5 ms instead of starting at full amplitude. A voice whose effect ended is no longer cut at the playhead: the `hum` and the `noise` fade out and their line is released once it has drained, so the stop is a fade and not a step. And the last 5 ms of a heartbeat recording fade out, so a beat ends without a click. The heartbeat's `duration` now decides when the sound stops *repeating*: when the instance ends, the thump stops repeating right there and the beat that is sounding plays to its end instead of being cut in the middle. No param renamed, no wire, UBO or API change, `PROTOCOL_VERSION` unchanged. Documented on the [heartbeat](guide/effects/audio/heartbeat.md) page. **Not verified in game**.
- **Synthesised voices no longer stutter or crackle on slow frames.** The `hum`/`noise`/`heartbeat` voices counted samples from frame ticks, so an unfocused window (a few frames per second) pushed each frame past the 4-tick cap and the voice emitted nothing (chunk-gap-chunk), and the 0.25 s line buffer underran whenever the game loop stalled. Each voice now counts samples on its own steady wall clock gated by the effect clock: slow frames and replay seeks emit the wall-clock debt sized to the line's free buffer space (the write never blocks the frame), phases stay continuous so there are no bursts or boundary clicks, and pause (or a paused replay) still freezes synthesis exactly as before. The buffer grows to 0.5 s. No param renamed, no UBO or wire change, `PROTOCOL_VERSION` unchanged. Documented on the [hum](guide/effects/audio/hum.md), [noise](guide/effects/audio/noise.md) and [heartbeat](guide/effects/audio/heartbeat.md) pages; audible output cannot be verified headless here. **Not verified in game**.
- **Local-contrast halos around geometry in `halftone`, `comic` and `accent_glow` — local averaging removed.** The 8-tap averaging ring made the tone mapping a de-facto edge detector: near any luminance step the local mean skewed and the tone saturated into flat paper-coloured bands with no dots, which reads as glowing outlines and is useless for halftones. All three effects now map absolute pixel luminance instead (`halftone` keeps its adjustable `gain` contrast around mid grey, default `2.5` = neutral; `comic` gains a tunable `mid` split point, default `0.5`; `accent_glow` gains tunable `mid`/`core` thresholds, defaults `0.5`/`0.68`), so halos cannot form by construction — plus 8 fewer texture fetches per pixel. Dot grids grow from the screen centre instead of a corner, so changing `cell_size` rescales around the middle of the frame. Three new optional params (`mid` on `comic`; `mid`, `core` on `accent_glow`), no UBO or wire change, `PROTOCOL_VERSION` unchanged. **Not verified in game**.
### Added
- **`hex_grid`: a triangular grid coloured by the screen position.** A new screen effect draws a triangular lattice over the frame - three line directions (two at 60 degrees, one vertical) on a `cell_size` cell (46 px by default) anchored at the screen centre - and paints the lines with the prism ramp picked by the position (`prism7(x * 0.6 + y * 0.4 + time * speed)`, drifting at 0.5 per second): the lines at 28 % of that colour over near-black `(0.01, 0.008, 0.02)`, and the highlights of the picture (brighter than `core`, 0.68 by default - absolute, not local, so no outline appears at a brightness step) in white; `amount` cross-fades the whole look (default `1.0`, neutral `0.0` so `fade_ticks` fades it) and no param has an upper cap - only the lower guards that avoid degenerate maths (`cell_size >= 1` px, `width >= 0.0001`). Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged. Documented in the [guide](guide/effects/screen/hex-grid.md). Asserted by `scripts/check-vfx-schema.ps1`, `scripts/check-shader-identifiers.ps1` and `scripts/check-builtins-parse.ps1`; the GLSL/pixels cannot be verified headless here. **Not verified in game**.
- **`spectral`: a rainbow prism look over the frame.** A new screen effect draws the picture through a seven-stop prism ramp - red `(1.0, 0.07, 0.1)`, orange `(1.0, 0.42, 0.03)`, yellow `(1.0, 0.88, 0.06)`, green `(0.1, 0.95, 0.28)`, blue `(0.06, 0.48, 1.0)`, violet `(0.28, 0.14, 1.0)`, magenta `(0.74, 0.16, 1.0)` - swept by the angle around `center_x`/`center_y`, the distance from it (0.9 per screen height) and the clock (0.7 per second); `mode` picks the variant: **0** the prism duotone (the frame is split into a dark and a bright half by its own brightness, `mid` 0.5 by default - absolute, not local, so no outline appears at a brightness step; the bright half takes the prism colour and the highlights, `core` 0.68, go white), **1** the chromatic smear (seven taps along the outward direction, spread by `0.0035 + 0.004 * distance`, every tap's edge `smoothstep(0.02, 0.30)` painted with its own prism stop - a wide ramp, so it stays a smear and not a drawn outline) and **2** the spectral ring (the luminance picks a band `floor(luma * 6.999)`, the distance picks a ring `floor(distance * 9 - time * 6)`, the two together pick a spectral stop and the highlights (`core` 0.68) go white; `amount` cross-fades the whole look (default `1.0`, neutral `0.0` so `fade_ticks` fades it), `mode` is an enum clamped to 0..2 and no other param has an upper cap. Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged. Documented in the [guide](guide/effects/screen/spectral.md). Asserted by `scripts/check-vfx-schema.ps1`, `scripts/check-shader-identifiers.ps1` and `scripts/check-builtins-parse.ps1`; the GLSL/pixels cannot be verified headless here. **Not verified in game**.
- **`torn_edge`: a ragged tear with a glowing line down it.** A new screen effect cuts the frame along a straight line through `center_x`/`center_y` whose angle is re-rolled every beat (`speed`, 20 per second by default): a value-noise ripple of `ragged` (0.02 of the screen height, 38 cycles across the tear) makes the edge wander, the two sides of the cut are drawn at opposite brightness (a pixel brighter than `mid`, 0.5 by default - the split is absolute, not local, so no outline appears at a brightness step), and the tear itself is a white line (an exponential falloff of `glow`, 0.0035 of the screen height, scaled by 1.6); `amount` cross-fades the whole look (default `1.0`, neutral `0.0` so `fade_ticks` fades it), and no param has an upper cap - only the lower guards that avoid degenerate maths (`speed >= 0`, `glow >= 0.0005`). Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged. Documented in the [guide](guide/effects/screen/torn-edge.md). Asserted by `scripts/check-vfx-schema.ps1`, `scripts/check-shader-identifiers.ps1` and `scripts/check-builtins-parse.ps1`; the GLSL/pixels cannot be verified headless here. **Not verified in game**.
- **`datamosh`: the frame is decoded against a reference that never arrives, and the movement search runs once per cell instead of once per pixel.** A new screen effect is a *feedback* effect: it keeps the picture it produced last frame in the frame's history buffer, and that history is the reference the decoder gets instead of a key frame. **The key frame never arrives**: the history only refreshes through the per-piece I-frame below, so the decoder keeps painting the screen as it was half a second ago. The movement is estimated the way a codec searches its reference frame for the matching block: one Lucas-Kanade step between the live picture and the reference (the reference carries the previous live frame's luma in its alpha) gives the velocity of the content, and a greedy search over 8, 4, 2 and 1 pixel finds the integer vector that best matches it, with a penalty that gives exactly zero where nothing moves. The search used to run per pixel (about 174 texture reads per pixel, a full-resolution pass the frame rate could not afford): every pixel of a piece needs the same vector, so the search now runs in its own pass at 1/8 resolution - one texel per 8x8 cell - and the update pass reads the result back. The picture is unchanged, the cost is about 6.5 reads per pixel instead of 174. The decoder then carries the pixels over as they are (point sampled, no filter, no leftover of the live frame), so a moving object drags sharp pixels of the old background with it; the luma is taken at the measured vector and the colour at `1 + chroma` times it, the way a 4:2:0 stream smears colour when it is decoded against the wrong reference - every colour is the picture's own, there is no random noise. The pieces come from a static hash quadtree (8 to 64 px), so neighbouring pieces differ in size and the layout never changes, and each piece returns to the real picture on its own phase every `iframe` seconds, so there is no global flash. `smear` (1) is the vector multiplier and `chroma` (0.5) the colour pull; only the lower guards exist (`chroma` and `amount` clamped to 0..1, `smear` and `iframe` not negative), no param has an upper cap. Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged. Honest limits, stated on the page: the melt lives in the same history buffer as `afterimage`, so the two chain when both run and each overwrites the alpha the other needs; the reference is the screen's own past rather than another shot; the movement needs some structure in the picture, so in flat areas (sky, a plain wall) nothing is dragged; and the motion field is one texel per 8x8 cell, so a slower movement than 8 px is not resolved. Documented in the [guide](guide/effects/screen/datamosh.md). Asserted by `scripts/check-vfx-schema.ps1`, `scripts/check-shader-identifiers.ps1`, `scripts/check-post-fusion.ps1` and `scripts/check-builtins-parse.ps1`; the GLSL/pixels cannot be verified headless here. **Not verified in game**.
- **`heartbeat`: a heartbeat positioned in the world.** A new audio effect plays a recording of one lub-dub that ships inside the mod (`rate` beat rate in bpm, default `60`; `volume` linear gain `0..1`, neutral `0.0` so `fade_ticks` fades it; `pitch` playback-speed multiplier, default `1` = as recorded) straight into the OS mixer — no resource pack needed, identical on every Minecraft line. Every beat restarts the recording, and the recording is time-scaled to the tempo: at 60 bpm it plays as recorded (0.85 s in a 1 s beat) and at 140 bpm it is compressed to about 0.36 s in a 0.43 s beat, with the pause inside the lub-dub closing up in the same proportion. The scaling keeps the tone where it was, so a fast heart is a fast recording on the same pitch rather than a higher one; `pitch` is a separate tone multiplier that does move the pitch; the pause between beats is whatever the recording leaves free, not a separate re-timing. A cut playback is faded over 5 ms so a restart never clicks, and `rate` and `pitch` are read fresh every tick, so keyframes/`expr`/bindings ride them live and the phase integrates per sample, so a rate change glides. Positioning, distance falloff (full within 8 blocks, silent past 48), clock semantics (pause freezes, seek resyncs silently), fade-out on single stop and immediate `stopAll`/world exit/client shutdown close all mirror the `hum`/`noise` voices; the same honest limits apply (vanilla mixer bypassed, no-audio-device stays silent with a one-time warning). Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged, no new dependency. Documented in the [guide](guide/effects/audio/heartbeat.md). Asserted by `scripts/check-vfx-schema.ps1` and `scripts/check-builtins-parse.ps1`; audible output cannot be verified headless here. **Not verified in game**.
- **`volume_duck`: scale a vanilla sound category down while active.** A new global (non-positional) audio effect sets one game-options category level to its pre-duck level times `volume` (`0..1`, neutral `1.0` so `fade_ticks` fades the duck in and out), selected by the `channel` param as a case-insensitive name (`master`, `music`, `records`, `weather`, `blocks`, `hostile`, `neutral`, `players`, `ambient`, `voice`, `ui`) or the historical `SoundSource` ordinal (`0..10`, back-compat); an unknown name warns once and safely no-ops instead of ducking the wrong category. Each frame only ducked channels are written (base x last-writer-wins scale across stacked ducks); the captured level is restored the frame a channel's last duck ends and on single stop, replay seek, `stopAll`, world exit and client shutdown, and nothing is ever written to `options.txt`. Honest limits, stated on the page: the options slider shows the scaled level and a mid-duck slider drag on that channel is overwritten, then lost on restore. Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged, no new dependency. Documented in the [guide](guide/effects/audio/volume-duck.md). Asserted by `scripts/check-vfx-schema.ps1` and `scripts/check-builtins-parse.ps1`; audible output cannot be verified headless here. **Not verified in game**.
- **`noise`: generated white noise positioned in the world.** A new audio effect synthesises mono white noise per effect instance (`volume` linear gain `0..1`, neutral `0.0` so `fade_ticks` fades it; `brightness` crisp/soft blend `0..1`, default `0.5`; `jitter` random-walk step with `jitter_rate` speed multiplier for a playback-rate and volume wander; `seed` reseeds the texture) straight into the OS mixer — no audio file in any pack, identical on every Minecraft line. Positioning, distance falloff (full within 8 blocks, silent past 48), clock semantics (pause freezes, seek resyncs silently), fade-out on single stop and immediate `stopAll`/world exit/client shutdown close all mirror the `hum` voice; the same honest limits apply (vanilla mixer bypassed, no-audio-device stays silent with a one-time warning). Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged, no new dependency. Documented in the [guide](guide/effects/audio/noise.md). Asserted by `scripts/check-vfx-schema.ps1` and `scripts/check-builtins-parse.ps1`; audible output cannot be verified headless here. **Not verified in game**.
- **`hum`: a generated sine hum positioned in the world.** A new audio effect synthesises a mono sine tone per effect instance (`frequency` in Hz, default `440`; `volume` linear gain `0..1`, neutral `0.0` so `fade_ticks` fades it) straight into the OS mixer — no audio file in any pack, identical on every Minecraft line. The voice is positioned like a world overlay (move position, first static `positions` entry, first entity anchor, authored `pos_x/y/z`, else the local-player snapshot) with a linear distance falloff (full within 8 blocks, silent past 48); it follows the shared effect clock, so pause freezes it and a replay seek resyncs silently. Stopping one instance fades it out; `stopAll`/world exit/client shutdown closes the voices at once. Honest limits, stated on the page: the vanilla mixer is bypassed (volume sliders do not apply — use `volume`), and a machine with no audio device stays silent with a one-time warning. Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged, no new dependency. Documented in the [guide](guide/effects/audio/hum.md). Asserted by `scripts/check-vfx-schema.ps1` and `scripts/check-builtins-parse.ps1`; audible output cannot be verified headless here. **Not verified in game**.
### Fixed
- **The frame rate no longer reaches the sound: the voices moved to their own audio thread.** The `hum`, `noise` and `heartbeat` voices were synthesised on the render thread, one chunk per frame, so the frame rate leaked into the sound: the gain ramps were one frame long, a long frame could starve the line's buffer (that is the crackling), and the heartbeat's time-scale work spiked the frame. The voices now live on a dedicated audio thread that generates fixed 5 ms blocks from its own wall clock and keeps every line about 60 ms ahead of its playhead, so a slow frame, a lost frame or an unfocused window cannot starve a line or stretch a ramp - the sound is the same at 30 fps and at 200 fps. The render thread only publishes the parameters and the shared effect clock; the voice state belongs to the audio thread, so there is nothing to lock. A paused game (or a paused replay) is still the one thing that stops it: the shared effect clock freezes and the thread renders silence until it moves again. The heartbeat's time-scale work moved to the audio thread with the rest. No param renamed, no wire, UBO or API change, `PROTOCOL_VERSION` unchanged. Documented on the [hum](guide/effects/audio/hum.md), [noise](guide/effects/audio/noise.md) and [heartbeat](guide/effects/audio/heartbeat.md) pages. **Not verified in game**.
- **The synthesised voices no longer crackle, and a heartbeat plays out instead of being cut.** Three click sources are gone. A voice that resumes after a silence gap (a pause, a game hitch, a fade) now fades in over 5 ms instead of starting at full amplitude. A voice whose effect ended is no longer cut at the playhead: the `hum` and the `noise` fade out and their line is released once it has drained, so the stop is a fade and not a step. And the last 5 ms of a heartbeat recording fade out, so a beat ends without a click. The heartbeat's `duration` now decides when the sound stops *repeating*: when the instance ends, the thump stops repeating right there and the beat that is sounding plays to its end instead of being cut in the middle. No param renamed, no wire, UBO or API change, `PROTOCOL_VERSION` unchanged. Documented on the [heartbeat](guide/effects/audio/heartbeat.md) page. **Not verified in game**.
- **Synthesised voices no longer stutter or crackle on slow frames.** The `hum`/`noise`/`heartbeat` voices counted samples from frame ticks, so an unfocused window (a few frames per second) pushed each frame past the 4-tick cap and the voice emitted nothing (chunk-gap-chunk), and the 0.25 s line buffer underran whenever the game loop stalled. Each voice now counts samples on its own steady wall clock gated by the effect clock: slow frames and replay seeks emit the wall-clock debt sized to the line's free buffer space (the write never blocks the frame), phases stay continuous so there are no bursts or boundary clicks, and pause (or a paused replay) still freezes synthesis exactly as before. The buffer grows to 0.5 s. No param renamed, no UBO or wire change, `PROTOCOL_VERSION` unchanged. Documented on the [hum](guide/effects/audio/hum.md), [noise](guide/effects/audio/noise.md) and [heartbeat](guide/effects/audio/heartbeat.md) pages; audible output cannot be verified headless here. **Not verified in game**.
- **Local-contrast halos around geometry in `halftone`, `comic` and `accent_glow` — local averaging removed.** The 8-tap averaging ring made the tone mapping a de-facto edge detector: near any luminance step the local mean skewed and the tone saturated into flat paper-coloured bands with no dots, which reads as glowing outlines and is useless for halftones. All three effects now map absolute pixel luminance instead (`halftone` keeps its adjustable `gain` contrast around mid grey, default `2.5` = neutral; `comic` gains a tunable `mid` split point, default `0.5`; `accent_glow` gains tunable `mid`/`core` thresholds, defaults `0.5`/`0.68`), so halos cannot form by construction — plus 8 fewer texture fetches per pixel. Dot grids grow from the screen centre instead of a corner, so changing `cell_size` rescales around the middle of the frame. Three new optional params (`mid` on `comic`; `mid`, `core` on `accent_glow`), no UBO or wire change, `PROTOCOL_VERSION` unchanged. **Not verified in game**.
### Added
- **`hex_grid`: a triangular grid coloured by the screen position.** A new screen effect draws a triangular lattice over the frame - three line directions (two at 60 degrees, one vertical) on a `cell_size` cell (46 px by default) anchored at the screen centre - and paints the lines with the prism ramp picked by the position (`prism7(x * 0.6 + y * 0.4 + time * speed)`, drifting at 0.5 per second): the lines at 28 % of that colour over near-black `(0.01, 0.008, 0.02)`, and the highlights of the picture (brighter than `core`, 0.68 by default - absolute, not local, so no outline appears at a brightness step) in white; `amount` cross-fades the whole look (default `1.0`, neutral `0.0` so `fade_ticks` fades it) and no param has an upper cap - only the lower guards that avoid degenerate maths (`cell_size >= 1` px, `width >= 0.0001`). Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged. Documented in the [guide](guide/effects/screen/hex-grid.md). Asserted by `scripts/check-vfx-schema.ps1`, `scripts/check-shader-identifiers.ps1` and `scripts/check-builtins-parse.ps1`; the GLSL/pixels cannot be verified headless here. **Not verified in game**.
- **`spectral`: a rainbow prism look over the frame.** A new screen effect draws the picture through a seven-stop prism ramp - red `(1.0, 0.07, 0.1)`, orange `(1.0, 0.42, 0.03)`, yellow `(1.0, 0.88, 0.06)`, green `(0.1, 0.95, 0.28)`, blue `(0.06, 0.48, 1.0)`, violet `(0.28, 0.14, 1.0)`, magenta `(0.74, 0.16, 1.0)` - swept by the angle around `center_x`/`center_y`, the distance from it (0.9 per screen height) and the clock (0.7 per second); `mode` picks the variant: **0** the prism duotone (the frame is split into a dark and a bright half by its own brightness, `mid` 0.5 by default - absolute, not local, so no outline appears at a brightness step; the bright half takes the prism colour and the highlights, `core` 0.68, go white), **1** the chromatic smear (seven taps along the outward direction, spread by `0.0035 + 0.004 * distance`, every tap's edge `smoothstep(0.02, 0.30)` painted with its own prism stop - a wide ramp, so it stays a smear and not a drawn outline) and **2** the spectral ring (the luminance picks a band `floor(luma * 6.999)`, the distance picks a ring `floor(distance * 9 - time * 6)`, the two together pick a spectral stop and the highlights (`core` 0.68) go white; `amount` cross-fades the whole look (default `1.0`, neutral `0.0` so `fade_ticks` fades it), `mode` is an enum clamped to 0..2 and no other param has an upper cap. Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged. Documented in the [guide](guide/effects/screen/spectral.md). Asserted by `scripts/check-vfx-schema.ps1`, `scripts/check-shader-identifiers.ps1` and `scripts/check-builtins-parse.ps1`; the GLSL/pixels cannot be verified headless here. **Not verified in game**.
- **`torn_edge`: a ragged tear with a glowing line down it.** A new screen effect cuts the frame along a straight line through `center_x`/`center_y` whose angle is re-rolled every beat (`speed`, 20 per second by default): a value-noise ripple of `ragged` (0.02 of the screen height, 38 cycles across the tear) makes the edge wander, the two sides of the cut are drawn at opposite brightness (a pixel brighter than `mid`, 0.5 by default - the split is absolute, not local, so no outline appears at a brightness step), and the tear itself is a white line (an exponential falloff of `glow`, 0.0035 of the screen height, scaled by 1.6); `amount` cross-fades the whole look (default `1.0`, neutral `0.0` so `fade_ticks` fades it), and no param has an upper cap - only the lower guards that avoid degenerate maths (`speed >= 0`, `glow >= 0.0005`). Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged. Documented in the [guide](guide/effects/screen/torn-edge.md). Asserted by `scripts/check-vfx-schema.ps1`, `scripts/check-shader-identifiers.ps1` and `scripts/check-builtins-parse.ps1`; the GLSL/pixels cannot be verified headless here. **Not verified in game**.
- **`datamosh`: the frame is decoded against a reference that never arrives, and the movement search runs once per cell instead of once per pixel.** A new screen effect is a *feedback* effect: it keeps the picture it produced last frame in the frame's history buffer, and that history is the reference the decoder gets instead of a key frame. **The key frame never arrives**: the history only refreshes through the per-piece I-frame below, so the decoder keeps painting the screen as it was half a second ago. The movement is estimated the way a codec searches its reference frame for the matching block: one Lucas-Kanade step between the live picture and the reference (the reference carries the previous live frame's luma in its alpha) gives the velocity of the content, and a greedy search over 8, 4, 2 and 1 pixel finds the integer vector that best matches it, with a penalty that gives exactly zero where nothing moves. The search used to run per pixel (about 174 texture reads per pixel, a full-resolution pass the frame rate could not afford): every pixel of a piece needs the same vector, so the search now runs in its own pass at 1/8 resolution - one texel per 8x8 cell - and the update pass reads the result back. The picture is unchanged, the cost is about 6.5 reads per pixel instead of 174. The decoder then carries the pixels over as they are (point sampled, no filter, no leftover of the live frame), so a moving object drags sharp pixels of the old background with it; the luma is taken at the measured vector and the colour at `1 + chroma` times it, the way a 4:2:0 stream smears colour when it is decoded against the wrong reference - every colour is the picture's own, there is no random noise. The pieces come from a static hash quadtree (8 to 64 px), so neighbouring pieces differ in size and the layout never changes, and each piece returns to the real picture on its own phase every `iframe` seconds, so there is no global flash. `smear` (1) is the vector multiplier and `chroma` (0.5) the colour pull; only the lower guards exist (`chroma` and `amount` clamped to 0..1, `smear` and `iframe` not negative), no param has an upper cap. Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged. Honest limits, stated on the page: the melt lives in the same history buffer as `afterimage`, so the two chain when both run and each overwrites the alpha the other needs; the reference is the screen's own past rather than another shot; the movement needs some structure in the picture, so in flat areas (sky, a plain wall) nothing is dragged; and the motion field is one texel per 8x8 cell, so a slower movement than 8 px is not resolved. Documented in the [guide](guide/effects/screen/datamosh.md). Asserted by `scripts/check-vfx-schema.ps1`, `scripts/check-shader-identifiers.ps1`, `scripts/check-post-fusion.ps1` and `scripts/check-builtins-parse.ps1`; the GLSL/pixels cannot be verified headless here. **Not verified in game**.
- **`heartbeat`: a heartbeat positioned in the world.** A new audio effect plays a recording of one lub-dub that ships inside the mod (`rate` beat rate in bpm, default `60`; `volume` linear gain `0..1`, neutral `0.0` so `fade_ticks` fades it; `pitch` playback-speed multiplier, default `1` = as recorded) straight into the OS mixer — no resource pack needed, identical on every Minecraft line. Every beat restarts the recording, and the recording is time-scaled to the tempo: at 60 bpm it plays as recorded (0.85 s in a 1 s beat) and at 140 bpm it is compressed to about 0.36 s in a 0.43 s beat, with the pause inside the lub-dub closing up in the same proportion. The scaling keeps the tone where it was, so a fast heart is a fast recording on the same pitch rather than a higher one; `pitch` is a separate tone multiplier that does move the pitch; the pause between beats is whatever the recording leaves free, not a separate re-timing. A cut playback is faded over 5 ms so a restart never clicks, and `rate` and `pitch` are read fresh every tick, so keyframes/`expr`/bindings ride them live and the phase integrates per sample, so a rate change glides. Positioning, distance falloff (full within 8 blocks, silent past 48), clock semantics (pause freezes, seek resyncs silently), fade-out on single stop and immediate `stopAll`/world exit/client shutdown close all mirror the `hum`/`noise` voices; the same honest limits apply (vanilla mixer bypassed, no-audio-device stays silent with a one-time warning). Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged, no new dependency. Documented in the [guide](guide/effects/audio/heartbeat.md). Asserted by `scripts/check-vfx-schema.ps1` and `scripts/check-builtins-parse.ps1`; audible output cannot be verified headless here. **Not verified in game**.
- **`volume_duck`: scale a vanilla sound category down while active.** A new global (non-positional) audio effect sets one game-options category level to its pre-duck level times `volume` (`0..1`, neutral `1.0` so `fade_ticks` fades the duck in and out), selected by the `channel` param as a case-insensitive name (`master`, `music`, `records`, `weather`, `blocks`, `hostile`, `neutral`, `players`, `ambient`, `voice`, `ui`) or the historical `SoundSource` ordinal (`0..10`, back-compat); an unknown name warns once and safely no-ops instead of ducking the wrong category. Each frame only ducked channels are written (base x last-writer-wins scale across stacked ducks); the captured level is restored the frame a channel's last duck ends and on single stop, replay seek, `stopAll`, world exit and client shutdown, and nothing is ever written to `options.txt`. Honest limits, stated on the page: the options slider shows the scaled level and a mid-duck slider drag on that channel is overwritten, then lost on restore. Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged, no new dependency. Documented in the [guide](guide/effects/audio/volume-duck.md). Asserted by `scripts/check-vfx-schema.ps1` and `scripts/check-builtins-parse.ps1`; audible output cannot be verified headless here. **Not verified in game**.
- **`noise`: generated white noise positioned in the world.** A new audio effect synthesises mono white noise per effect instance (`volume` linear gain `0..1`, neutral `0.0` so `fade_ticks` fades it; `brightness` crisp/soft blend `0..1`, default `0.5`; `jitter` random-walk step with `jitter_rate` speed multiplier for a playback-rate and volume wander; `seed` reseeds the texture) straight into the OS mixer — no audio file in any pack, identical on every Minecraft line. Positioning, distance falloff (full within 8 blocks, silent past 48), clock semantics (pause freezes, seek resyncs silently), fade-out on single stop and immediate `stopAll`/world exit/client shutdown close all mirror the `hum` voice; the same honest limits apply (vanilla mixer bypassed, no-audio-device stays silent with a one-time warning). Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged, no new dependency. Documented in the [guide](guide/effects/audio/noise.md). Asserted by `scripts/check-vfx-schema.ps1` and `scripts/check-builtins-parse.ps1`; audible output cannot be verified headless here. **Not verified in game**.
- **`hum`: a generated sine hum positioned in the world.** A new audio effect synthesises a mono sine tone per effect instance (`frequency` in Hz, default `440`; `volume` linear gain `0..1`, neutral `0.0` so `fade_ticks` fades it) straight into the OS mixer — no audio file in any pack, identical on every Minecraft line. The voice is positioned like a world overlay (move position, first static `positions` entry, first entity anchor, authored `pos_x/y/z`, else the local-player snapshot) with a linear distance falloff (full within 8 blocks, silent past 48); it follows the shared effect clock, so pause freezes it and a replay seek resyncs silently. Stopping one instance fades it out; `stopAll`/world exit/client shutdown closes the voices at once. Honest limits, stated on the page: the vanilla mixer is bypassed (volume sliders do not apply — use `volume`), and a machine with no audio device stays silent with a one-time warning. Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change, `PROTOCOL_VERSION` unchanged, no new dependency. Documented in the [guide](guide/effects/audio/hum.md). Asserted by `scripts/check-vfx-schema.ps1` and `scripts/check-builtins-parse.ps1`; audible output cannot be verified headless here. **Not verified in game**.
### Fixed
- **The frame rate no longer reaches the sound: the voices moved to their own audio thread.** The `hum`, `noise` and `heartbeat` voices were synthesised on the render thread, one chunk per frame, so the frame rate leaked into the sound: the gain ramps were one frame long, a long frame could starve the line's buffer (that is the crackling), and the heartbeat's time-scale work spiked the frame. The voices now live on a dedicated audio thread that generates fixed 5 ms blocks from its own wall clock and keeps every line about 60 ms ahead of its playhead, so a slow frame, a lost frame or an unfocused window cannot starve a line or stretch a ramp - the sound is the same at 30 fps and at 200 fps. The render thread only publishes the parameters and the shared effect clock; the voice state belongs to the audio thread, so there is nothing to lock. A paused game (or a paused replay) is still the one thing that stops it: the shared effect clock freezes and the thread renders silence until it moves again. The heartbeat's time-scale work moved to the audio thread with the rest. No param renamed, no wire, UBO or API change, `PROTOCOL_VERSION` unchanged. Documented on the [hum](guide/effects/audio/hum.md), [noise](guide/effects/audio/noise.md) and [heartbeat](guide/effects/audio/heartbeat.md) pages. **Not verified in game**.
- **The synthesised voices no longer crackle, and a heartbeat plays out instead of being cut.** Three click sources are gone. A voice that resumes after a silence gap (a pause, a game hitch, a fade) now fades in over 5 ms instead of starting at full amplitude. A voice whose effect ended is no longer cut at the playhead: the `hum` and the `noise` fade out and their line is released once it has drained, so the stop is a fade and not a step. And the last 5 ms of a heartbeat recording fade out, so a beat ends without a click. The heartbeat's `duration` now decides when the sound stops *repeating*: when the instance ends, the thump stops repeating right there and the beat that is sounding plays to its end instead of being cut in the middle. No param renamed, no wire, UBO or API change, `PROTOCOL_VERSION` unchanged. Documented on the [heartbeat](guide/effects/audio/heartbeat.md) page. **Not verified in game**.
- **Synthesised voices no longer stutter or crackle on slow frames.** The `hum`/`noise`/`heartbeat` voices counted samples from frame ticks, so an unfocused window (a few frames per second) pushed each frame past the 4-tick cap and the voice emitted nothing (chunk-gap-chunk), and the 0.25 s line buffer underran whenever the game loop stalled. Each voice now counts samples on its own steady wall clock gated by the effect clock: slow frames and replay seeks emit the wall-clock debt sized to the line's free buffer space (the write never blocks the frame), phases stay continuous so there are no bursts or boundary clicks, and pause (or a paused replay) still freezes synthesis exactly as before. The buffer grows to 0.5 s. No param renamed, no UBO or wire change, `PROTOCOL_VERSION` unchanged. Documented on the [hum](guide/effects/audio/hum.md), [noise](guide/effects/audio/noise.md) and [heartbeat](guide/effects/audio/heartbeat.md) pages; audible output cannot be verified headless here. **Not verified in game**.
- **Local-contrast halos around geometry in `halftone`, `comic` and `accent_glow` — local averaging removed.** The 8-tap averaging ring made the tone mapping a de-facto edge detector: near any luminance step the local mean skewed and the tone saturated into flat paper-coloured bands with no dots, which reads as glowing outlines and is useless for halftones. All three effects now map absolute pixel luminance instead (`halftone` keeps its adjustable `gain` contrast around mid grey, default `2.5` = neutral; `comic` gains a tunable `mid` split point, default `0.5`; `accent_glow` gains tunable `mid`/`core` thresholds, defaults `0.5`/`0.68`), so halos cannot form by construction — plus 8 fewer texture fetches per pixel. Dot grids grow from the screen centre instead of a corner, so changing `cell_size` rescales around the middle of the frame. Three new optional params (`mid` on `comic`; `mid`, `core` on `accent_glow`), no UBO or wire change, `PROTOCOL_VERSION` unchanged. **Not verified in game**.
### Added
- **`particles` shape params, `dust` size, `block_chain` spacing/scale, outline widths, `light_beam` top_scale, `pulse_ring` thickness, `guide_line` width/dashes, spark speed/size and `entity_displace` scale no longer have an upper cap.** Every lifted param keeps only its lower guard (`particles`: `radius >= 0.05`, `height >= 0.5`, `turns >= 0.25`, `speed/accel/lifetime >= 0`, `vel_y` unbounded, `dust size >= 0.05`; `block_chain`: `spacing >= 0.25`, `scale >= 0.1`; outlines/ring/guide-line widths and thickness/gap `>= 0`, `dash_length >= 0.1`; `light_beam top_scale >= 0.1`; spark `speed >= 0`, `size >= 0.01`; `entity_displace scale >= 0.5` mirroring the renderer floor), and the editor schema mirrors the same floor-only bounds. The floors that remain are the ones that avoid degenerate geometry or division by zero (spacing, dash_length, displace scale); no geometry loop count depends on any lifted param, so the caps had no perf basis. No datapack field renamed, no wire, UBO or API change, `PROTOCOL_VERSION` unchanged. Documented on [particles](guide/effects/world/particles.md), [block_chain](guide/effects/world/block-chain.md), [light_beam](guide/effects/world/light-beam.md), [entity_displace](guide/effects/entity/entity-displace.md) and [spark presets](guide/datapack/sparks.md).
- **`light_beam` radius/height and `pulse_ring` radius no longer have an upper cap.** `light_beam` keeps its lower guards (`radius >= 0.1`, `height >= 1`) and `pulse_ring` keeps `radius >= 0` (degenerate/inverted geometry guard), but the `16`/`256` and `64` caps are gone: the geometry cost is constant (a fixed shell/slice count), so radius/height only scale positions. No datapack field renamed, no wire, UBO or API change, `PROTOCOL_VERSION` unchanged. Documented on [light_beam](guide/effects/world/light-beam.md) and [pulse_ring](guide/effects/world/pulse-ring.md).
### Added
- **`particles` shape params, `dust` size, `block_chain` spacing/scale, outline widths, `light_beam` top_scale, `pulse_ring` thickness, `guide_line` width/dashes, spark speed/size and `entity_displace` scale no longer have an upper cap.** Every lifted param keeps only its lower guard (`particles`: `radius >= 0.05`, `height >= 0.5`, `turns >= 0.25`, `speed/accel/lifetime >= 0`, `vel_y` unbounded, `dust size >= 0.05`; `block_chain`: `spacing >= 0.25`, `scale >= 0.1`; outlines/ring/guide-line widths and thickness/gap `>= 0`, `dash_length >= 0.1`; `light_beam top_scale >= 0.1`; spark `speed >= 0`, `size >= 0.01`; `entity_displace scale >= 0.5` mirroring the renderer floor), and the editor schema mirrors the same floor-only bounds. The floors that remain are the ones that avoid degenerate geometry or division by zero (spacing, dash_length, displace scale); no geometry loop count depends on any lifted param, so the caps had no perf basis. No datapack field renamed, no wire, UBO or API change, `PROTOCOL_VERSION` unchanged. Documented on [particles](guide/effects/world/particles.md), [block_chain](guide/effects/world/block-chain.md), [light_beam](guide/effects/world/light-beam.md), [entity_displace](guide/effects/entity/entity-displace.md) and [spark presets](guide/datapack/sparks.md).
- **`light_beam` radius/height and `pulse_ring` radius no longer have an upper cap.** `light_beam` keeps its lower guards (`radius >= 0.1`, `height >= 1`) and `pulse_ring` keeps `radius >= 0` (degenerate/inverted geometry guard), but the `16`/`256` and `64` caps are gone: the geometry cost is constant (a fixed shell/slice count), so radius/height only scale positions. No datapack field renamed, no wire, UBO or API change, `PROTOCOL_VERSION` unchanged. Documented on [light_beam](guide/effects/world/light-beam.md) and [pulse_ring](guide/effects/world/pulse-ring.md).
### Added
- **`particles` shape params, `dust` size, `block_chain` spacing/scale, outline widths, `light_beam` top_scale, `pulse_ring` thickness, `guide_line` width/dashes, spark speed/size and `entity_displace` scale no longer have an upper cap.** Every lifted param keeps only its lower guard (`particles`: `radius >= 0.05`, `height >= 0.5`, `turns >= 0.25`, `speed/accel/lifetime >= 0`, `vel_y` unbounded, `dust size >= 0.05`; `block_chain`: `spacing >= 0.25`, `scale >= 0.1`; outlines/ring/guide-line widths and thickness/gap `>= 0`, `dash_length >= 0.1`; `light_beam top_scale >= 0.1`; spark `speed >= 0`, `size >= 0.01`; `entity_displace scale >= 0.5` mirroring the renderer floor), and the editor schema mirrors the same floor-only bounds. The floors that remain are the ones that avoid degenerate geometry or division by zero (spacing, dash_length, displace scale); no geometry loop count depends on any lifted param, so the caps had no perf basis. No datapack field renamed, no wire, UBO or API change, `PROTOCOL_VERSION` unchanged. Documented on [particles](guide/effects/world/particles.md), [block_chain](guide/effects/world/block-chain.md), [light_beam](guide/effects/world/light-beam.md), [entity_displace](guide/effects/entity/entity-displace.md) and [spark presets](guide/datapack/sparks.md).
- **`light_beam` radius/height and `pulse_ring` radius no longer have an upper cap.** `light_beam` keeps its lower guards (`radius >= 0.1`, `height >= 1`) and `pulse_ring` keeps `radius >= 0` (degenerate/inverted geometry guard), but the `16`/`256` and `64` caps are gone: the geometry cost is constant (a fixed shell/slice count), so radius/height only scale positions. No datapack field renamed, no wire, UBO or API change, `PROTOCOL_VERSION` unchanged. Documented on [light_beam](guide/effects/world/light-beam.md) and [pulse_ring](guide/effects/world/pulse-ring.md).
### Added
- **`particles` shape params, `dust` size, `block_chain` spacing/scale, outline widths, `light_beam` top_scale, `pulse_ring` thickness, `guide_line` width/dashes, spark speed/size and `entity_displace` scale no longer have an upper cap.** Every lifted param keeps only its lower guard (`particles`: `radius >= 0.05`, `height >= 0.5`, `turns >= 0.25`, `speed/accel/lifetime >= 0`, `vel_y` unbounded, `dust size >= 0.05`; `block_chain`: `spacing >= 0.25`, `scale >= 0.1`; outlines/ring/guide-line widths and thickness/gap `>= 0`, `dash_length >= 0.1`; `light_beam top_scale >= 0.1`; spark `speed >= 0`, `size >= 0.01`; `entity_displace scale >= 0.5` mirroring the renderer floor), and the editor schema mirrors the same floor-only bounds. The floors that remain are the ones that avoid degenerate geometry or division by zero (spacing, dash_length, displace scale); no geometry loop count depends on any lifted param, so the caps had no perf basis. No datapack field renamed, no wire, UBO or API change, `PROTOCOL_VERSION` unchanged. Documented on [particles](guide/effects/world/particles.md), [block_chain](guide/effects/world/block-chain.md), [light_beam](guide/effects/world/light-beam.md), [entity_displace](guide/effects/entity/entity-displace.md) and [spark presets](guide/datapack/sparks.md).
- **`light_beam` radius/height and `pulse_ring` radius no longer have an upper cap.** `light_beam` keeps its lower guards (`radius >= 0.1`, `height >= 1`) and `pulse_ring` keeps `radius >= 0` (degenerate/inverted geometry guard), but the `16`/`256` and `64` caps are gone: the geometry cost is constant (a fixed shell/slice count), so radius/height only scale positions. No datapack field renamed, no wire, UBO or API change, `PROTOCOL_VERSION` unchanged. Documented on [light_beam](guide/effects/world/light-beam.md) and [pulse_ring](guide/effects/world/pulse-ring.md).