DSP
← devlog

The 2× button that did nothing

Niner 1.0 is out. It is a free, open-source, three-layer synthesized kick drum in every format. The reason it took a 1.0 rather than another point release is a UI bug I had been quietly avoiding: the in-DAW scale control did not work.

Niner has a small UI N× control. In the standalone it scaled the interface fine. Loaded as a plugin, clicking 1.5× or 2× did something worse than nothing. The window grew to the right size, but the controls stayed small and pinned to the top-left corner, with the right-hand cluster stranded against the new window edge. It looked broken because it was.

What was actually happening

Niner’s UI is egui rendered through egui-baseview, the layer that bridges egui to a plugin window. Scaling an egui UI is meant to be one call, ctx.set_zoom_factor(2.0), and egui composes that with the window’s native scale to get its effective pixels-per-point: the conversion between logical layout units and physical pixels.

The catch is that egui-baseview was not reading egui’s zoom factor at all. It derived a single pixels-per-point straight from the window’s scale policy and used that one value everywhere: for the logical canvas it handed egui, for tessellation, and for mapping the mouse. So set_zoom_factor moved egui’s internal notion of scale, but the integration overrode it on both the input and the output side. The zoom was inert.

That explains the screenshot exactly. The window was physically 2×, because the plugin had asked the host to resize it, but the content was laid out and rendered at 1×. So it filled a quarter of the frame, and the controls anchored to the right edge drifted out to meet the new border.

The fix

Every other egui backend, egui-winit and eframe included, derives its pixels-per-point as zoom_factor × native_scale. egui-baseview just needed to do the same. The change composes the native scale factor with egui’s zoom factor into one effective pixels-per-point and uses it in the three places that matter:

  • the logical screen rect handed to egui, so a fixed layout fills the window instead of a corner;
  • tessellation, so shapes render at the right physical size;
  • pointer input. baseview reports the cursor in logical coordinates, so dividing by the zoom factor keeps clicks landing on the control under the cursor. Miss this one and the UI scales perfectly while every knob grabs from the wrong place.

The whole thing is a no-op at the default zoom of 1.0. Effective equals native, so every existing user of the library is byte-for-byte unchanged. It only does anything once you opt in with a zoom factor. There is a small bonus too. Because egui still knows the device scale, it re-rasterises its font atlas at the effective scale, so text at 2× stays crisp instead of turning into a bilinear smear of the 1× glyphs.

It ships with four unit tests: the identity case at zoom 1.0, the canvas collapsing back to base size when the window grows, native and zoom composing under a HiDPI scale, and a center click mapping to the center of the scaled canvas. The change lives in a fork, Hornfisk/egui-baseview on the respect-zoom-factor branch, pinned into Niner via Cargo’s [patch]. The upstream GitHub repo is an archived mirror of a Codeberg project, so upstreaming this cleanly is a follow-up rather than a pull request I could fire off today.

Also in 1.0

With the scaling sorted, the rest of the release is the visual pass it was holding up: Blender-baked, runtime-tinted photoreal knobs with a soft contact shadow and a little wear, a flat-dark faceplate with engraved section dividers, and a new icon.

Niner 1.0 is free and GPL-3.0. VST3, CLAP and Standalone, on Linux, macOS and Windows. Grab it here, or play the sound samples in your browser first.