MOOR security

Dieser Eintrag ist auf Englisch verfasst.

Wide and antialiased lines on an M1: every line becomes a rectangle

Entry 048807 of 918 tests pass

D3D12's wide, narrow and alpha-antialiased lines are now drawn on an M1 Mac as rectangles made by a generated object function, and D24 depth buffers get D3D12's depth bias units.

What changed

D3D12 has four ways to draw a line. ALIASED is the thin line every GPU draws. QUADRILATERAL_WIDE and QUADRILATERAL_NARROW cover a rectangle 1.4 or 1.0 pixels wide between the endpoints. ALPHA_ANTIALIASED fades the line's edges: the pixel shader's alpha is multiplied by how much of the pixel the line covers. Older pipelines select these modes with two bits, MultisampleEnable and AntialiasedLineEnable. Metal only has aliased lines. Its multisampled lines didn't match any of D3D12's models in a spike that drew 300 random lines (see the chart).

The layer now draws lines in the other three modes itself. A generated object function (a Metal compute stage that runs before the rasterizer) takes each line and emits two triangles that cover the rectangle. For ALPHA lines, the rectangle is 2 pixels wide with half-pixel caps, and each corner carries its distance across and along the line. A SPIRV-Cross patch multiplies the shader's output alpha by the box-filtered coverage computed from those distances.

The session's first commit made D3D12 depth bias work on 24-bit depth buffers. D3D12 counts the constant bias in units of 1/(2^24 − 1) for D24 formats. Metal uses the float format's units, so the layer adds the missing part as a small clip-space offset per viewport.

One line through the object function: clip, map to pixels, the rectangle per mode, and the coverage multiply.Line-list or strip draw, mode WIDE, NARROW or ALPHA → Vertex shader captures its outputs; Vertex shader captures its outputs → Object function: one thread per line; Object function: one thread per line → Clip to w ≥ 10⁻⁶ and to the viewport + 8 px, in clip space; map to pixels with the first vertex's viewport; Clip to w ≥ 10⁻⁶ and to the viewport + 8 px, in clip space; map to pixels with the first vertex's viewport → mode; mode → Rectangle 1.4 px wide (WIDE); mode → Rectangle 1.0 px wide (NARROW, or ALPHA without a float4 o0); mode → 2 px wide + ½ px caps (+ √2/2 with MSAA), corners carry across, along and length (ALPHA); Rectangle 1.4 px wide → Two clockwise triangles, offsets scaled by w; Rectangle 1.0 px wide → Two clockwise triangles, offsets scaled by w; 2 px wide + ½ px caps (+ √2/2 with MSAA), corners carry across, along and length → Two clockwise triangles, offsets scaled by w; Two clockwise triangles, offsets scaled by w → Metal rasterizes (no depth bias); Metal rasterizes (no depth bias) → ALPHA?; ALPHA? → Pixel shader (no); ALPHA? → Pixel shader, then o0.a × box-filtered coverage; discard where it is 0 (yes)Line-list or strip draw,mode WIDE, NARROWor ALPHAVertex shadercaptures its outputsObject function:one thread per lineClip to w ≥ 10⁻⁶ and to theviewport + 8 px, in clip space;map to pixels with thefirst vertex's viewportmodeRectangle1.4 px wideRectangle1.0 px wide2 px wide + ½ px caps(+ √2/2 with MSAA),corners carry across,along and lengthTwo clockwise triangles,offsets scaled by wMetal rasterizes(no depth bias)ALPHA?Pixel shaderPixel shader, theno0.a × box-filtered coverage;discard where it is 0WIDENARROW, or ALPHAwithout a float4 o0ALPHAnoyes
One line through the object function: clip, map to pixels, the rectangle per mode, and the coverage multiply. (Mermaid source)
Two enlarged crops of white wireframe lines on dark grey. Left, the lines of a frustum end short of its apex. Right, the reference, where the lines meet at the apex and one continues to the right edge.
Bgfx 29-debugdraw near the frustum's apex, enlarged 3 times. Left, with only the w cut: the edge to the right starts about 70 pixels late. Right, with the clip.
Gallery scene lines: a 64x36 target enlarged 10 times, with a pixel grid and the exact segments as white lines: five spokes drawn as ALIASED (top left, Metal's own lines), QUADRILATERAL_NARROW (top right, 1 pixel wide), QUADRILATERAL_WIDE (bottom left, 1.4 pixels) and ALPHA_ANTIALIASED (bottom right, coverage blended into the background) lines; animated, the spokes turn a fifth of a circle. All scenes in the gallery

The interesting part

The new line tests all passed. Then the full run failed two bgfx examples, one of them with a frustum edge that was about 70 pixels short next to its visible end (the second picture).

The frustum's far corner is just behind the camera, at w = −0.49. The object function cut each line at w = 10^-6 to drop the part behind the eye. That was correct, but the cut point projects to x/w ≈ 10^9 pixels. At that size a 32-bit float can only step in 64 or 128 pixel increments. The ALPHA mode measures the distance along the line from that far end, so its coverage near the visible end was off by tens of pixels.

Random lines and a scaled-down copy of the frustum edge didn't reproduce it. It only happens with a vertex barely behind the eye and a large x. The fix clips the line in clip space, before any division, to the viewport plus 8 pixels on each side, so nothing reaching the divide is far off screen. A new test case draws exactly this line. Without the fix it has 17 wrong pixels.

The rest of the two examples' difference was correct. bgfx asks for antialiased lines, and D3D12 draws them alpha-antialiased. The references came from bgfx's own Metal renderer, whose lines are aliased. After a review showed that every differing pixel is on a line, both references became reviewed mtl12 frames.

Numbers

  • Full run: 807 pass, 25 fail, 86 skip of 918; 1 newly failing, 4 newly passing. The newly failing test, vkd3d-proton test_rasterizer_ordered_views_dxil, is intermittent. It failed once in 43 runs this session, and passed in the full run just before at the same code (808 pass, 0 newly failing). It is still an open question.
  • Previous entry's run: 804 pass, 26 fail, 86 skip of 916.
  • api/line_rasterization: 303 checks over 23 cases. vkd3d-proton test_line_rasterization and test_depth_bias_formats pass now.
  • The spike: of 300 random lines, how many Metal's multisampled lines matched each D3D12 model at 1 and 4 samples.
  • The frustum edge: 17 wrong pixels in the new test case with the w cut alone, 0 with the clip. The cases in front of the eye fail 8 checks if noperspective members are mixed at the clip-space fraction instead of the screen's.
Tests by suite at the end of this entry: 807 of 918 pass (data as CSV)
  • pass
  • fail
  • skip

suite · share of each row's total

  1. vkd3d-proton (+1)479 pass, 16 fail, 82 skip of 577
  2. vkd3d177 pass, 9 fail, 4 skip of 190
  3. apps77 pass, 0 fail, 0 skip of 77
  4. api (+2)70 pass, 0 fail, 0 skip of 70
  5. samples4 pass, 0 fail, 0 skip of 4
Which model Metal's lines match: lines of 300 identical, sample by sample (data as CSV)
  • 1 sample
  • 4 samples

model · identical lines of 300

  1. NARROW97 1 sample, 63 4 samples
  2. WIDE7 1 sample, 1 4 samples
  3. ALIASED297 1 sample, 1 4 samples
  4. per sample297 1 sample, 7 4 samples
  5. parallelogram163 1 sample, 289 4 samples