MOOR security

Dieser Eintrag ist auf Englisch verfasst.

A GPU clock that counted in the wrong unit, and samplers that take pixel coordinates

Entry 017Quick subset: 61 of 89 pass

Under Rosetta, mach_absolute_time() counts nanoseconds while Metal's GPU timestamps count 24 MHz ticks, so the CPU/GPU clock calibration was off by a factor of about 41.7.

What changed

mtl12 translates Direct3D 12 to Apple's Metal so that Windows games can run under Wine on Apple Silicon. This session added two features:

  • ID3D12Device15 and CPU-resolved query heaps. A query heap created with D3D12_QUERY_HEAP_FLAG_CPU_RESOLVE can be read with ResolveQueryData, with no command list involved. Timestamps come from MTL4CounterHeap resolveCounterRange, and other heap types from the results they already keep in CPU-visible memory. The new api test query_heap_cpu_resolve was written first and failed with E_NOINTERFACE.
  • Non-normalized sampler coordinates for heap samplers. D3D12 lets a sampler take texel coordinates instead of 0–1 coordinates, and D3D12_FEATURE_DATA_D3D12_OPTIONS17 reports it. Metal supports it for samplers in argument buffers too, but its validation layer rejects some combinations. mtl12 now cleans up descriptions that also break D3D12's own rules. A review found a padding hole in the sampler cache key.

The interesting part

GetClockCalibration returns a GPU timestamp and a CPU timestamp taken at the same moment. Applications use the pair to place GPU timings on the CPU timeline. mtl12 returned mach_absolute_time() for both, and an earlier arm64 spike showed that Metal's counter-heap timestamps use the same timebase.

vkd3d-proton's test_query_heap_cpu_resolve_timestamp compares a resolved timestamp with the calibration value, and the two were about 41.7 times apart. The cause: mtl12's Unix side is x86_64 and runs under Rosetta, and there the process timebase changes. A probe built for both architectures showed this:

x86_64 (Rosetta)arm64
mach_timebase_info1/1125/3
mach_absolute_time()nanoseconds24 MHz ticks
MTL4CounterHeap timestamp24 MHz ticks24 MHz ticks

Rosetta rescales mach_absolute_time() to nanoseconds so x86 code that assumes a 1/1 timebase keeps working. The GPU doesn't know about Rosetta, so its counters stay in ticks. 41.7 is 1e9 / 24e6. The spike had run arm64-native, where the two clocks happen to match.

The fix converts the CPU value into ticks through the timebase, using 128-bit arithmetic. On arm64 the conversion does nothing. -[MTLDevice sampleTimestamps:gpuTimestamp:] is no help here, because it reports nanoseconds for both values.

Two clocks in different units, and the conversion now applied.mach_absolute_time() x86_64 under Rosetta: nanoseconds → GetClockCalibration; Metal counter heap 24 MHz ticks everywhere → GetClockCalibration; GetClockCalibration → 41.7× apart (1e9 / 24e6) ✘ (before: units mixed); GetClockCalibration → same unit ✔ (now: CPU value converted to ticks through the timebase)mach_absolute_time()x86_64 under Rosetta: nanosecondsGetClockCalibrationMetal counter heap24 MHz ticks everywhere41.7× apart(1e9 / 24e6) ✘same unit ✔before: units mixednow: CPU value converted to ticksthrough the timebase
Two clocks in different units, and the conversion now applied. (Mermaid source)
One millisecond, as each clock counts it (41.7x = 1e9 / 24e6) (data as CSV)

clock · counts per millisecond

  1. mach_absolute_time, x86_64 (Rosetta)1,000,000
  2. mach_absolute_time, arm6424,000
  3. MTL4CounterHeap, both24,000

Numbers

  • No full run this session. --fast at the start: 61 pass, 5 fail, 23 skip of 89, 0 newly failing.
  • Newly passing: vkd3d-proton test_query_heap_cpu_resolve_timestamp and test_query_heap_cpu_resolve_occlusion, test_sampler_non_normalized_coordinates, and two new api tests (query_heap_cpu_resolve, sampler_non_normalized).