Skip to content

fix(dice): publish unconfirmed-clock devices and offer every CLOCK_CAPS rate without EAP (#189, #223) - #222

Merged
mrmidi merged 3 commits into
mainfrom
fix/dice-advisory-clock-wait
Oct 23, 2026
Merged

mrmidi merged 3 commits into
mainfrom
fix/dice-advisory-clock-wait

Conversation

@mrmidi

@mrmidi mrmidi commented Oct 22, 2026 •

Copy link
Copy Markdown
Owner

Fixes the regression reported in #189, and offers every supported rate on DICE devices without EAP (#223): a PreSonus FireStudio 2626 appeared in CoreAudio on build 19 and not on builds 21 or 33.

Cause

Since 21e01580 (between builds 19 and 21), a DICE device is published only after clock preparation succeeds: owner claim, GLOBAL_ENABLE=0, CLOCK_SELECT, then polling GLOBAL until the clock is confirmed. Any failure there refused the device (geometry-load-failed), and nothing retried until the next bus reset. The build-33 dump in #189 shows our owner claim and CLOCK_ACCEPTED, then nothing.

The preparation made fatal what the TCAT driver treats as advisory. PaeFireStudio RestartStreaming (0xd8e2), the driver this device shipped with, waits up to 150 ms for CLOCK_ACCEPTED and up to 1 s for lock, keeps waiting through a failed STATUS read, logs a timeout and creates the streams anyway. Only a failed write stops it. Ours ended on a single failed GLOBAL poll read, on no acceptance within 150 ms, and at the 3 s deadline.

Change

  • A failed GLOBAL poll read is retried until the deadline. A stale generation or a cancellation still ends the preparation.
  • 150 ms without CLOCK_ACCEPTED is logged, and the wait for a settled STATUS continues.
  • At the deadline the last GLOBAL state is logged and the device is described and published.
  • Unchanged: failed writes, a foreign owner and an unsupported version still fail. Stream start keeps its own lock gate, so an unlocked device is published but not enabled.

Which of these the 2626 actually hit is not known without its log. The new [DiceInit] clock unconfirmed … / no CLOCK_ACCEPTED … lines will show it.

Rates on DICE devices without EAP (#223)

A DICE device without the EAP extension never offered 88.2/96 kHz. The ZED-R16 in #223 and this 2626 (CLOCK_CAPS 0x13FF001E) are two examples. Such a device reports channel layouts only for its current rate, in the TX/RX stream registers. We offered a rate only when we knew its layout, so only the attach rate appeared in CoreAudio. The layout for another rate was learned only when the device changed rate by itself (#190), never when the host asked for that rate. The closed PR #224 worked around this for one device with a hard-coded table.

The TCAT driver works without that knowledge. Saffire.kext performFormatChange checks the rate against CLOCK_CAPS. RestartStreaming then sets CLOCK_SELECT and waits for the clock. Finally PopulateDeviceStruct reads the TX/RX layout and CreateStreams rebuilds the audio streams. The layout is read after the switch, not before. This branch now does the same:

  • Without EAP, every CLOCK_CAPS rate is announced. A rate whose layout has not been read carries the current rate's layout as a placeholder, marked LayoutProvisional.
  • When the host selects such a rate, the transaction sets the clock and waits for lock as before. It then reads the layout from the TX/RX registers, then installs and publishes it. Only a layout read from the device is ever installed or streamed. A rate whose layout cannot be read is rolled back like any failed rate change.
  • A placeholder never overwrites a layout that was read, including after a bus reset.
  • Because a placeholder's real layout is unknown, capacity for it is reserved at the protocol limit (64 channels). A device attached at 96 kHz can then still switch to 48 kHz, where it has more channels.
  • EAP devices are unchanged, since they already describe every rate.

Not tested on hardware. Open question: how apps react when the channel count changes after a rate switch. EAP devices already change channel count at that point.

Fixture

The 2626's register dump from #189 is added as a test fixture. It is the first captured DICE device with no catalog row, so it goes through the generic DICE fallback: two streams each way (16 + 10 PCM), no EAP, 8 clock source names. pydice now accepts 1 to 13 names. The build-19 report is used because pydice does not parse the build-33 report format yet.

Testing

  • New tests on the 2626 image: a failed poll read (busy, timeout) and an unconfirmed clock complete; the device is published through the DICE adapter despite a failed poll read. An unlocked Venice F24 completes preparation and its start does not enable.
  • All four fail on the old code; the adapter test fails with geometry-load-failed.
  • clock-never-locks goldens updated: the start now runs and times out on its own lock gate, ending with ENABLE=0 and no streams, as before.
  • Rates without EAP:
    • Device level (2626 image whose registers follow the rate mode): the host selects 96 kHz, the layout is read after the switch and replaces the placeholder. 88.2 kHz stays a placeholder, and 48 kHz keeps its layout. After a bus reset and re-describe, both read layouts survive.
    • Unit tests:
      • every CLOCK_CAPS rate is announced;
      • attached at 2x, the 1x rates are placeholders;
      • no placeholder is created without a known current layout;
      • a read layout replaces only a placeholder;
      • a placeholder rate is settled only by its locked clock;
      • a placeholder reserves the protocol limit;
      • the flag survives the nub crossing.
    • 10 of 10 injected mutations are caught.
  • C++ suite 3805 pass, pydice 148 pass, dext compiles for both architectures.
  • Not run on hardware yet.

🤖 Generated with Claude Code

mrmidi and others added 3 commits October 22, 2026 14:45
The 2626 is the first captured DICE device with no catalog row: it
publishes through the generic DICE fallback. Two streams each way
(16 + 10 PCM, MIDI on stream 0), no EAP, and its firmware lists 8 clock
source names, not 13. pydice accepted only 13; it now accepts 1 to 13.
The fixture is the build-19 report from the issue (the build-33 report
has the same registers in a newer format pydice does not parse yet).

SessionShape can carry a real GUID, and with it the unit directory's
Model_ID: the generic fallback checks the GUID's DICE layout, which the
synthetic OUI-prefixed GUID does not follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since 21e01580 (between builds 19 and 21) publication waits for clock
preparation, and any failure there refused the device ("geometry load
failed") with no retry until the next bus reset. Issue #189: a
FireStudio 2626 published on build 19 and no longer appeared in
CoreAudio; its build-33 dump shows our owner claim and CLOCK_ACCEPTED,
then nothing.

The preparation made fatal what the TCAT driver treats as advisory.
PaeFireStudio RestartStreaming (0xd8e2), the driver this device shipped
with, waits up to 150 ms for CLOCK_ACCEPTED and up to 1 s for lock,
keeps waiting through a failed STATUS read, logs a timeout and creates
the streams anyway; only a failed write stops it. 21e01580 itself
describes the lock wait as advisory. Ours ended on one failed GLOBAL
poll read, on no acceptance within 150 ms, and at the 3 s deadline.

Now:
- a failed GLOBAL poll read is retried until the deadline; a stale
  generation or a cancellation still ends the preparation;
- 150 ms without CLOCK_ACCEPTED logs and goes on to wait for a settled
  STATUS, as before;
- at the deadline the preparation logs the last GLOBAL state and
  completes, so the device is described and published.
Failed writes, a foreign owner and an unsupported version still fail.
Stream start keeps its own lock gate: an unlocked device is published
but not enabled (clock-never-locks goldens: the start now runs and
times out on that gate, ending with ENABLE=0 and no streams, as before).

Which failure the 2626 hit is not known without its log; the tests
cover each one on its captured registers.

Tests: a failed poll read (busy, timeout) and an unconfirmed clock on
the 2626 image complete; the 2626 is published through a failed poll
read via the DICE adapter; an unlocked Venice F24 completes preparation
and its start does not enable. All four fail on the old code, the
adapter test with "geometry-load-failed".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A DICE device without EAP describes its channel layout only for the
current rate, so only the attach rate was ever offered (ZED-R16 in #223,
FireStudio 2626 in #189). Follow the TCAT driver instead: announce every
CLOCK_CAPS rate, set the clock, then read the layout (Saffire.kext
performFormatChange -> RestartStreaming -> PopulateDeviceStruct ->
CreateStreams).

- A rate with an unread layout carries the current layout as a
  placeholder (RateFormation::layoutProvisional) for the HAL format list.
- A host rate change to it reads the layout once the clock locks and
  installs only that measured layout; an unreadable one rolls back.
- A placeholder never replaces a measured layout, also after a bus reset.
- Capacity for a placeholder is reserved at the protocol limit, so a
  device attached at 2x can still switch to 1x.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mrmidi mrmidi changed the title fix(dice): publish a device whose clock preparation is not confirmed (#189) fix(dice): publish unconfirmed-clock devices and offer every CLOCK_CAPS rate without EAP (#189, #223) Oct 23, 2026
@mrmidi
mrmidi merged commit 3dd7f65 into main Oct 23, 2026
2 checks passed
@mrmidi
mrmidi deleted the fix/dice-advisory-clock-wait branch October 23, 2026 10:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant