Investigating a Rendering Error in Rack 2 — NVG_HOLE & Layer 1

I’ve found that there are problems when drawing on Rack’s layer 1 if a path contains a sub-path with reverse (NVG_HOLE) path winding.

Background

For those unfamiliar with path winding in nanovg, it can be used to specify holes in objects. For example, this renders a simple donut shape, where the donut’s hole is half the radius of the donut:

nvgBeginPath(args.vg);
nvgCircle(args.vg, 0, 0, 10);
nvgCircle(args.vg, 0, 0, 5);
nvgPathWinding(args.vg, NVG_HOLE);
nvgFill(args.vg);

Donut-like shapes are a requirement for rendering halos on my ring lights.

Scenario A

The problem occurs under the following condition:

  • A sub-path with NVG_HOLE winding is drawn on layer 1
    • The error does not occur if this path is moved to either draw() or drawLayer() (if layer == 0)

Symptoms

When an overlay (like a Rack menu) is rendered above the hole-bearing layer-1 path, interference is visible on the overlay.

At first I thought I was seeing widget box boundaries, but later testing revealed it is an error in the outer border (stroke?) of the sub-path which precedes the NVG_HOLE winding. (The outer radial gradient here is housed in a rect.)

Scenario B

The criteria for Scenario A must be met, then:

  1. First instantiate a module which contains a hole-bearing layer-1 widget.
  2. Then instantiate Fundamental VCA.
  3. Reduce VCA level to zero, or otherwise turn-off its light by patching 0V into the CV input.
  4. Ensure the VCA remains on-screen along with your test module, to ensure they are both rendered.

Symptoms

What happens here is that the color of the light which was just turned off (the VCA’s slider) is “picked up” and rendered at the aforementioned boundaries.

In the following image, the two modules to the right of the VCA were added to the scene after the VCA. The two on the left were added before.

This color is certainly being pulled from the VCA’s light, because it was previously different, and it occurs with some other modules too:

Further

I’ve created a repository on GitHub with a test plugin, test modules, and test widgets. Two of the widgets (MonsterSquare and MonsterCircle) are designed to make the error as obvious as possible. This is accomplished by nesting many alternating NVG_SOLID and NVG_HOLE sub-paths:

These monstrous widgets helped to reveal a strange interaction: if the widgets are overlayed, the boxes (rather than the paths) of widgets on modules added to the scene later “temporarily fix” the error in widgets on modules added earlier:

Considering the hectic schedule of VCV at the moment, the difficulty in debugging this, and the fact that this presently only affects my plugin (maybe), I don’t anticipate that this is on the list for fixing prior to Rack 2 release. So, because I’ve got some time on my hands at present, I’m doing what I can to figure it out. Any help would be greatly appreciated!

These artifacts do look like they might be related to the issue that I had. Although that was not caused by an explicit NVG_HOLE.

image2 image1

The first attached image shows a bright white line in the menu. This is as a result of drawing a zero-height rectangle in a drawLayer method at that point on the screen.

In the second image, two such zero height rectangles are visible. The rectangle should be green (actually has a gradient) and it has been rendered as two rows of pixels, one above and one below the expected location. The white pixels visible between them were rendered as expected by the draw method. The location of the misrendered rectangle corresponds with the white lines rendered on the menu in the first image.

Note that in the second image, there are two zero-height rectangles. In the first image the left most rectangle has a non-zero height, and there is no corresponding white artefact in the menu.

I changed the colour of the line rendered at that point in the drawMethod, and the artefact change colour accordingly. But this is not the last colour used by the drawMethod.

This suggests that the two issues are related. Somehow the odd behaviour is punching a hole between the layers.

Some possibilities spring to mind:

  • The upper layers are failing to render anything in this area, leaving behind sections which were rendered below.
  • The information is being rendered into an incorrect part of the frame buffer, or an incorrect frame buffer (unlikely because the artefacts line up with the underlying layer)

I can’t see any direct relationship between NVG_HOLE and an undersized rectangle.

1 Like

Is that issue also alleviated by adjusting Rack’s bloom setting (halo brightness) to zero?

Changing the light bloom has no effect. However, changing the room brightness does change the brightness of the artefact.

So the orginally drawn colour is being overdrawn by the room dimming layer.

I can just possibly see a connection.

According to memononen, nanovg normalises the direction of paths. So irrespective of whether you draw them clockwise or counter-clockwise, they are all normalised. If you apply NVG_HOLE, the direction of the current path is reversed, and this allows the winding rule to produce the hole.

It’s possible that a rectangle with zero size is not being normalised (because it is degenerate and has no direction) But when the antialiasing inflation is applied, it results in a hole.

This looks like it might be my smoking gun.

A zero height rectangle has some pixel leakage, but it also screws up the stencil buffer. If antialiasing inflation is also causing issues with holes, then the same might apply for your problem.

1 Like

I’m seeing now that the effect is only bloom-dependent on Algomorph and not in the test modules. I must have missed something when I first attempted to verify bloom-dependence. Edited the original post.

Confirmed that adding this line removes artifacts:

image

Aside: I tested with this change in only one widget, and other widgets were unaffected. So it appears that ShapeAntiAlias does not need be be set True again prior to exiting the widget’s draw function.

I haven’t checked, but I would presume that Andrew uses either nvgSave/nvgRestore or nvgReset, to protect modules from each other. So I’m not surprised that you don’t need to turn it back on yourself.

So, in summary, probably a known issue in nanovg, and we both have workarounds for our problem (I just avoid filling degenerate rectangles)

Maybe! I haven’t tested the effect of disabling AA on my ring halos yet, and I expect it won’t be pretty so I expect this isn’t a solution for me.

Edit: Hmm, not so bad after all. I’ll roll with it.

Thanks a million! @carbon14

1 Like