# Kernel Streaming Safety Plan

Last updated: 2026-06-21

## Purpose

Kernel Streaming is in scope as a Windows low-level playback path for the
diagnostic tone tester, but it must be introduced in staged gates. KS can
address audio filters and pins below the user-mode endpoint APIs, so accidental
open/reopen loops, pin creation, or stream-state changes are treated as
high-risk device lifecycle operations.

## Staged Gates

1. Model-only support (task 039)
   - Add `BackendApi::kKernelStreaming`, UI labels, diagnostics fields, and
     snapshot serialization fields.
   - Do not enumerate KS filters.
   - Do not call `CreateFile`, `KsCreatePin`, `DeviceIoControl`, or any stream
     state transition.
   - Capability rows may be displayed only as unsupported/model-only evidence
     with explicit `opens_filter=false`, `creates_pin=false`, and
     `starts_stream=false` fields.

2. Discovery and non-playback pin/data-range probe (task 040)
   - Enumerate candidate render filters and pins with bounded scope.
   - Record filter symbolic link, pin ID, dataflow, communication, data-range
     count, format specifier, and any API error.
   - No playback pin is allowed to transition to running.
   - Every probe result must state whether it opened a filter and whether it
     created a pin.

3. PCM playback (task 041)
   - Start only one explicitly selected filter/pin/format combination.
   - Apply graceful stop, drain/hold behavior, cooldowns, and lifecycle logs.
   - Keep stream-open behavior must favor device stability over fast reopen.

4. DoP carrier playback (task 042)
   - Use only selected PCM carrier formats that are explicitly accepted by the
     KS data-range/probe layer.
   - Keep-alive silence must remain valid DoP marker silence when DoP is active.
   - Do not infer native DSD-over-KS support from DoP carrier acceptance.

5. GUI/diagnostic integration (task 043)
   - KS must follow the same device-first flow as other APIs.
   - Logs and bundles must expose KS filter/pin/data-range identity and all
     lifecycle result codes.
   - Automatic diagnostics must remain bounded and cooldown-gated.

## Required Snapshot Fields

Kernel Streaming discovery and capability records must preserve:

- `ks_filter_symbolic_link`
- `ks_pin_id`
- `ks_pin_dataflow`
- `ks_pin_communication`
- `ks_data_range_count`
- `ks_data_format_specifier`
- `opens_filter`
- `creates_pin`
- `starts_stream`
- `signal_mode`
- `keep_alive_format`
- `dop_dsd_rate_multiplier` when DoP is active
- `dop_marker_first` when DoP is active
- `ks_result_level`
- `ks_streaming_model`
- `ks_pin_interface_set`
- `ks_pin_interface_id`
- `supports_standard_streaming`
- `supports_looped_streaming`
- `supports_wavert_rtaudio_properties`
- `accepts_wavert_notification_buffer`
- `ks_playback_implementation`
- `ks_playback_path`
- `requested_access`
- `code_hex`
- `code_domain`
- `event_category`

Fields may use `not-enumerated` only during the task 039 model-only stage.

## Result Levels

KS capability cells distinguish:

- `data_range_match`: the selected pin's KSDATARANGE accepts the PCM/DoP
  carrier shape. This is a candidate, not confirmed playback support.
- `pin_create_verified`: `KsCreatePin` accepted the exact format and the pin
  was closed immediately without setting the stream to RUN.
- `start_verified`: a future or manual check has confirmed stream start. The
  current automated matrix does not claim this level.
- `unsupported`, `not_applicable`, and `error`: non-playable outcomes.

The automatic matrix may keep `data_range_match` cells selectable as candidates
with an explicit warning, but it must not label them as confirmed `available`.
If a candidate start fails, that is recorded as start verification failure
rather than a contradiction of a confirmed supported result.

Generic backend `FormatProbeResult::supported_formats` must not flatten
`data_range_match` rows into confirmed support. Only stronger evidence such as
`pin_create_verified` or `start_verified` may be exposed as generic confirmed
supported formats. Data-range matches remain visible in the matrix as selectable
candidates with warnings.

## Streaming Model Classification

Kernel Streaming pins must be classified by detected interface and WaveRT
properties before playback is attempted:

- `standard_packet`: the pin reports `KSINTERFACE_STANDARD_STREAMING`. This is
  an implemented playback path and is logged as `KS standard packet`. The
  backend writes packets through `DeviceIoControl(IOCTL_KS_WRITE_STREAM)`.
- `standard_looped`: the pin reports
  `KSINTERFACE_STANDARD_LOOPED_STREAMING` but not a currently implemented
  packet path. It must be shown as detected, playback not implemented, rather
  than unsupported.
- `wavert_cyclic`: the pin exposes WaveRT `KSPROPERTY_RTAUDIO_*` capability
  and accepts `KSPROPERTY_RTAUDIO_BUFFER_WITH_NOTIFICATION` for the selected
  render pin/format through a no-RUN probe using the looped interface. This path
  is implemented as WaveRT cyclic-buffer playback.
- `unknown`: neither the standard packet nor looped/WaveRT model was explicitly
  detected.

The `KSSTREAM_HEADER` plus `IOCTL_KS_WRITE_STREAM` implementation is standard
packet streaming. It must not be labeled as looped streaming or WaveRT
cyclic-buffer playback. WaveRT cyclic-buffer playback is a separate path that
uses `KSPROPERTY_RTAUDIO_BUFFER_WITH_NOTIFICATION`, registers a notification
event, writes valid PCM/DoP bytes into the returned cyclic buffer, uses memory
barriers when requested by the driver, and transitions ACQUIRE -> PAUSE -> RUN.
`supports_wavert_rtaudio_properties=true` only means one or more RtAudio
properties were detected. It does not make a matrix cell startable by itself.
Only `accepts_wavert_notification_buffer=true` together with
`KSINTERFACE_STANDARD_LOOPED_STREAMING` may classify the selected pin/format as
`wavert_cyclic` / implemented. Pure `standard_looped` support, and
BasicSupport-only WaveRT detection without an accepted notification buffer,
remain detected but playback not implemented.

## WaveRT Cyclic Playback

WaveRT playback may be attempted only after an explicit selected filter/pin and
format have passed the same device-first matrix flow as other KS paths. The
implemented path:

- creates the pin with `KSINTERFACE_STANDARD_LOOPED_STREAMING`;
- requests `KSPROPERTY_RTAUDIO_BUFFER_WITH_NOTIFICATION` with a bounded buffer
  request and notification count;
- registers `KSPROPERTY_RTAUDIO_REGISTER_NOTIFICATION_EVENT` when possible;
- primes the complete cyclic buffer with valid signal/silence before RUN;
- updates the cyclic buffer on notifications, or conservatively sleeps when
  notifications are unavailable;
- unregisters the notification event before closing the pin;
- logs buffer size, period size, notification count, memory-barrier flag, and
  runtime counters.

`KSPROPERTY_RTAUDIO_SETWRITEPACKET` is not required for the Lynx AES16e path
validated in this task; devices that require packet write accounting remain a
future compatibility refinement rather than an implicit fallback.

## Faulted Release Behavior

A faulted KS backend must still be releasable. Shutdown prioritizes stopping the
render thread and closing pin/filter handles. If handles are closed, the backend
can transition from `faulted` to `released` with a force-release lifecycle log
while preserving the original fault code/message in release event fields. This
prevents the GUI from trapping the user with an active faulted backend that
cannot be cleared.

## Synchronous Write / Release Timeout

The current playback backend writes KS packets through synchronous
`DeviceIoControl(IOCTL_KS_WRITE_STREAM)`. Because some drivers can stall inside
that call, shutdown and release must not wait forever for the render/write
thread. Release requests set the render stop flag, request cancellation of the
thread's synchronous I/O where Windows supports it, and then wait for a bounded
timeout.

If the render thread does not exit within the timeout, the backend records a
faulted release event with:

- `api=DeviceIoControl(IOCTL_KS_WRITE_STREAM)`
- `event_category=release`
- `code_domain=internal`
- `symbol=APTT_KS_RENDER_THREAD_JOIN_TIMEOUT`

In that state the backend must not close pin/filter handles or report
`released`, because the render thread may still own the pin handle. The UI must
keep the backend active/faulted so release can be retried after the driver
returns from the write call.

## Final Destructor Thread-Lifetime Policy

Normal UI release and final object destruction have intentionally different
policies.

- During normal UI stop/release, a render-thread join timeout leaves the backend
  active and faulted so the user can retry release after the driver returns.
  This path must not report `released` or close handles that may still be owned
  by the render thread.
- During final backend object destruction, `Impl::~Impl` must never return while
  the render `std::thread` is still joinable. The current policy is to request
  cancellation, then block until the render thread exits and can be joined. It
  does not detach, because the render thread captures the backend object and
  detach would risk use-after-free.
- Destructor cleanup may close handles only after the render thread has been
  joined. It does not turn a timed-out UI release into a successful `released`
  lifecycle report.

The destructor logs this distinction with release-category evidence:

- `api=KernelStreamingPcmBackend::~Impl`
- `destructor_blocks_until_render_thread_joined=true`
- `normal_release_timeout_retry_policy=backend_retained_faulted_for_retry`
- `detach_used=false`

## Thread-Safe Event Sequence Policy

KS runtime events can be emitted from both the UI/control thread and the
render/write thread. Event sequence assignment therefore uses an atomic
`DiagnosticSequenceCounter` instead of a plain integer. The ordering is
monotonic enough for log sorting and avoids data races when lifecycle, runtime,
error, and release events are appended concurrently.

## CancelSynchronousIo Evidence

Release and final destruction request cancellation of the render thread's
synchronous write with `CancelSynchronousIo` where Windows supports it. The
result is diagnostic evidence, not the authoritative release outcome. A
`CancelSynchronousIo` failure, including a no-pending-I/O style failure, is not
fatal by itself. The bounded render-thread join timeout remains the failure
condition.

The backend must not reinterpret `std::thread::native_handle()` as a Win32
`HANDLE`. On Windows builds the render thread duplicates its own current thread
pseudo-handle with `DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), ...)`
and stores that real thread handle for later cancellation. Normal successful
joins close the duplicated handle immediately after `join()`. If UI release
times out, the duplicated handle stays with the retained active/faulted backend
so a later release retry or final destructor cleanup can still request
cancellation. If the duplicated handle is unavailable, the cancellation event
records `cancel_synchronous_io_attempted=false` and
`symbol=APTT_KS_CANCEL_THREAD_HANDLE_UNAVAILABLE`.

Cancellation evidence uses:

- `api=CancelSynchronousIo`
- `event_category=release`
- `code_domain=win32`
- `code_signed`
- `code_hex`
- `symbol`
- `cancel_synchronous_io_attempted`
- `cancel_synchronous_io_succeeded`
- `cancel_synchronous_io_failure_is_fatal=false`
- `join_timeout_is_authoritative=true`

## GUI Close Guard

The GUI must not close the process while a backend is active or holding a
device. `WM_CLOSE` is intercepted and converted into a visible warning instead
of `DestroyWindow`/`PostQuitMessage` when any backend is active. The Japanese
warning for a generic active backend is:

`再生または保持中のデバイスがあります。先に停止または解放してください。`

If the active backend is Kernel Streaming, faulted, and the last error indicates
the bounded write-thread join timeout, the warning is:

`KS書き込みスレッドの終了待ちがタイムアウトしています。安全に解放できていない可能性があります。再度、解放を試してください。`

The guard does not force-close stuck KS I/O, does not detach the render thread,
and does not claim the backend was released. The user must stop/release first,
or retry release after the driver returns from the synchronous write.

## Error Code Evidence

Structured KS evidence keeps the signed numeric code in `code_signed`, but
`code_hex` for Win32, HRESULT, and NTSTATUS domains uses 8-digit 32-bit display
forms such as `0x00000005`, `0x8889000A`, and `0xC00000BB`. This avoids
sign-extended support-log values while preserving the raw signed code for
programmatic analysis.

## RunningSilence Policy

`RunningSilence` is a keep-warm state only. The KS backend does not resume
directly from `RunningSilence` to `RunningSignal`, because that would require
mutating PCM/DoP generator and marker state while the render thread may be
reading it. Stop can be a no-op while the keep-warm stream is still running, so
the user-facing Japanese reason is release-required:

`KS保持中ストリームからの即時信号再開は未対応です。保持中デバイスを解放してから再実行してください。`

Direct resume remains disabled.

## Original Fault Code-Domain Limitation

Faulted release events preserve `original_fault_code_signed`,
`original_fault_code_hex`, and `original_fault_message`, but the current
`BackendError` model does not carry the original `code_domain`. As a result,
`original_fault_code_hex` is still a generic signed-code hex rendering rather
than a domain-aware Win32/HRESULT/NTSTATUS 8-digit display. Primary KS error
events retain the domain-aware `code_domain` and `code_hex` fields; this
limitation applies only to the copied original-fault evidence on later release
events.

## Discovery / Probe Warning And Access

Kernel Streaming discovery/probe is not passive: it may open KS filters and
issue property queries. The GUI displays a non-modal warning near KS scan
controls. Query-only discovery/probe uses `GENERIC_READ` where possible and
logs `requested_access`; explicit pin creation probes may use
`GENERIC_READ|GENERIC_WRITE` and log the reason. Playback may still require
write access for `KsCreatePin` and stream writes.

## Non-Goals

- Do not change global Windows audio, power-management, or MMCSS registry
  settings.
- Do not use KS as an automatic fallback for WASAPI, DirectSound, MME, or ASIO.
- Do not infer native DSD support from PCM or DoP capability rows.
