MOOR security

Dieser Eintrag ist auf Englisch verfasst.

Buffer views that start in the middle of an element

Entry 071867 of 962 tests pass

mtl12 now supports Direct3D 12 buffer views given in bytes, including 12-byte-per-element views that start 4 or 8 bytes into an element, which Metal's texture buffers can't start at.

What changed

A view tells a shader which part of a buffer to read and how: as raw bytes, as an array of structs, or as "typed" elements of a format such as R32G32B32_UINT (three 32-bit numbers). Direct3D 12 has always described a view's range in whole elements. The newer byte-offset views (the OPTIONS22 capability CreateByteOffsetViewsSupported) give an offset and a size in bytes instead, with a size of 0 meaning "to the end of the buffer" (DirectX-Specs, "Revised CreateViews"). mtl12 reported the capability as unsupported until this session, so vkd3d-proton's test_buffer_descriptor_byte_offset skipped.

The function that writes a buffer view into a descriptor (a 64-byte record shaders read to find a resource) now takes bytes, checks the spec's alignment rules, and turns old element-based views into bytes as well.

An R32G32B32 view at byte offset 4 read from an R32 texture buffer at byte 0, with the shift in the descriptor record.byte-offset SRV, R32G32B32_UINT Offset 4, Size 36: three 12-byte elements → Metal texture buffer: R32, base byte 0 (16-byte aligned, as Metal needs); Metal texture buffer: R32, base byte 0 (16-byte aligned, as Metal needs) → texels: 0 | 1 2 3 | 4 5 6 | 7 8 9 | ... view element 0 starts at texel 1; texels: 0 | 1 2 3 | 4 5 6 | 7 8 9 | ... view element 0 starts at texel 1 → descriptor record word: 1 + shift = 1 + (4 mod 12) / 4 = 2; descriptor record word: 1 + shift = 1 + (4 mod 12) / 4 = 2 → spvTexelBufferRead(c) i = 3c + word - 1; spvTexelBufferRead(c) i = 3c + word - 1 → element 1 = texels 4, 5, 6 = bytes 16..27 of the bufferbyte-offset SRV, R32G32B32_UINTOffset 4, Size 36: three 12-byte elementsMetal texture buffer: R32, base byte 0(16-byte aligned, as Metal needs)texels: 0 | 1 2 3 | 4 5 6 | 7 8 9 | ...view element 0 starts at texel 1descriptor record word:1 + shift = 1 + (4 mod 12) / 4 = 2spvTexelBufferRead(c)i = 3c + word - 1element 1 = texels 4, 5, 6= bytes 16..27 of the buffer
An R32G32B32 view at byte offset 4 read from an R32 texture buffer at byte 0, with the shift in the descriptor record. (Mermaid source)
How legacy and byte-offset descriptions reach the descriptor record, by view kind.SRV/UAV description BUFFER_BYTE_OFFSET: Offset, Size in bytes → write_buffer_view: validate alignment, Size 0 = to the end, bounds; legacy BUFFER description FirstElement, NumElements → write_buffer_view: validate alignment, Size 0 = to the end, bounds (x element size); write_buffer_view: validate alignment, Size 0 = to the end, bounds → view kind?; view kind? → record: address + length (shader reads through a pointer) (raw / structured); view kind? → texture buffer per 2^27 elements + first element in the record (typed); texture buffer per 2^27 elements + first element in the record → + texel shift 0-2 (SPIRV-Cross patch 0044) (R32G32B32)SRV/UAV descriptionBUFFER_BYTE_OFFSET: Offset, Size in byteswrite_buffer_view: validate alignment,Size 0 = to the end, boundslegacy BUFFER descriptionFirstElement, NumElementsview kind?record: address + length(shader reads through a pointer)texture buffer per 2^27 elements+ first element in the record+ texel shift 0-2(SPIRV-Cross patch 0044)x element sizeraw / structuredtypedR32G32B32
How legacy and byte-offset descriptions reach the descriptor record, by view kind. (Mermaid source)

The interesting part

Raw and struct views were easy: the record already holds a plain address and a length. Typed views go through a Metal texture buffer, whose start must be 16-byte aligned. mtl12 already handles that by pointing the texture buffer at an aligned base and letting the shader add the view's first element.

R32G32B32 was the problem. Metal has no 12-byte texture-buffer format, so mtl12 reads such a view as a buffer of single 32-bit texels, three per element. The spec lets a 12-byte view start at any multiple of 4 bytes. At offset 4, element 0 starts at texel 1, element 1 at texel 4, and so on. No element index lands on those texels, and no aligned base helps: the only 16-byte boundary at or below byte 4 is byte 0.

The fix is one add in the shader. The record already had a word saying "this view is R32G32B32". It now holds 1 plus the shift (0, 1 or 2 texels), and a SPIRV-Cross patch reads element i from texel 3i + word − 1 (patch 0044). Every other format reads the same as before.

Numbers

  • Full run: 867 pass, 27 fail, 68 skip of 962, against 865/27/69 of 961 at the end of the last session; 2 newly passing (api/byte_offset_views, new, and vkd3d-proton test_buffer_descriptor_byte_offset, which skipped before), 0 newly failing.
  • api/byte_offset_views: 1082 checks. It reads three R32G32B32 views at offsets 4, 8 and 24, struct, raw, R32 and R16 views, writes through three UAVs including a struct counter, and clears a byte-offset UAV.
  • Mutations: without the shift, 6 api checks and 2 vkd3d-proton checks fail. Reading the UAV counter offset from the older, shorter description's place fails the counter check.
Tests by suite at the end of this entry: 867 of 962 pass (data as CSV)
  • pass
  • fail
  • skip

suite · share of each row's total

  1. vkd3d-proton (+1)498 pass, 14 fail, 65 skip of 577
  2. vkd3d178 pass, 9 fail, 3 skip of 190
  3. api (+1)91 pass, 0 fail, 0 skip of 91
  4. apps80 pass, 0 fail, 0 skip of 80
  5. wine-dxgi16 pass, 4 fail, 0 skip of 20
  6. samples4 pass, 0 fail, 0 skip of 4