Added layered rendering to the new renderer and implemented blurred backgrounds
2023-04-17
I got back from my vacation which was very relaxing! I didn't write any blog posts while I was away, but I did work some more on the new Neovide renderer. I added layers to the scene struct so that we can get windows working, added clipping for said layers, and added an optional naive blur to the background of the layers.
Scene Layers
One of the goals I have for the new renderer is to make it easy to capture or record scene objects into a serialized form that can be written to disk. I have a dream of storing a rolling buffer of the rendered frames that can then be rendered to disc on a panic and then drawn back using a utility tool afterward.
So with that goal in mind, the scene struct needs to be flexible enough to record everything necessary to draw the whole frame. A key part for Neovide in particular is to render each window as a layer on the screen.
So now rather than storing instances of each primitive at the root of the struct, a list of layers with clipping, background color, and blur details is stored in the scene. Then in the renderer, we loop over each layer calling each of the drawables in order. Here's the high level structure in the render function:
if let Some = &mut self.surface
Clipping
Clipping is particularly useful for rendering lists of elements that can be partially cut off. Luckily, scissor rects are a common feature of graphics apis and let you specify what should get rendered for a given render pass.
let mut render_pass = encoder.begin_render_pass;
if let Some = layer.clip
drawable.draw;
Blurred Backgrounds
With layers and clipping working, I now needed to setup the
blurred backgrounds for Neovide. After some fiddling about,
I decided to add a blur flag to the InstancedQuad
struct.
Due to bytemuck constraints, boolean values are not allowed
because they cannot be initialized from any possible binary
value. So I ended up going for a u32 as the alignment would
cause issues anyway. If non zero, I would then use the
offscreen texture I setup for the subpixel text rendering to
compute a gaussian blur.
I didn't know a ton about how gaussian blurs worked other than that they use something called a kernel to aggregate the values of surrounding pixels in the source texture to compute the blur. The trick is picking what factor to multiply each of the pixels by when doing the summation.
After some quick googling I found some weights that I put in a constant table that seem to make a reasonable result. The shape of a gaussian blur's kernel weights have a number of symmetries. For my usecase, I took advantage of symmetry across both axes to reduce the number of weights I had to list.
const GUASSIAN_WEIGHT_FACTOR: f32 = 1. / 1003.;
const GUASSIAN_WEIGHTS: = ;
const GUASSIAN_RADIUS: i32 = 3;
Then in the fragment shader for the quad, I check the blur field and loop over a 9x9 patch of the surface texture looking up the weights in the table.
I decided to modify the quad shader because much of the code is reused for a blurred rectangle. Some concerns I have about this approach though:
- Having a branch at the root of the fragment shader is a bad look. Since the instances might differ, I think there is performance being left on the table.
- The severity of the blur isn't configurable. Because the table is a lookup and changes with the blur size, I didn't really have a good way for making it dynamic. This likely requires just doing some more math, but I postponed this for now.
- Gaussian blurs are famously separable meaning you can do the vertical slice, and the horizontal slice of the patch separately and combine them which is much more efficient. I didn't implement this for now as performance isn't my highest priority right this moment, but this is something to come back to.
With the blur completed, I wrote up a test scene to demonstrate the effect which is the image at the top of the post!
From here, I think the majority of the features necessary to implement Neovide's renderer contract are complete! Remaining tasks are to add rendering of arbitrary paths (for drawing the cursor) and to add a higher level render api like the one exposed via DrawCommands in Neovide.
Till tomorrow,
Kaylee