AudioBackendType::WasapiExclusive, offered on Windows, plus the selection arm
that opens the exclusive stream before the shared one - the same shape ALSA
Direct has on Linux.
No separate enumeration was needed: 249adc77b already made AudioDevice.id the
WASAPI ENDPOINT ID, which is exactly what WasapiDirectStream::new wants. That
fix turns out to have been the load-bearing one; by friendly name this could
not have addressed a device at all.
Two deliberate fallbacks, both of which prefer a resampled track to silence:
- No device chosen: there is nothing to open exclusively, so it says so and
uses the shared stream. Grabbing the default DAC would be deciding for the
user.
- The endpoint refuses the rate: warn and fall through. A DAC that will not
take 192/24 in exclusive still plays it shared.
The mode LED lights EXCL only when a device IS chosen, because with none the
player falls through to shared - a lit EXCL there would be the LED lying,
which is the one thing output_labels must never do.
backendIsWasapi joins the settings document, which is what unhides the
Exclusive-mode and DSD rows that fc0d35e29 gated on it.
229 + 71 + 110 tests pass; five QML audits pass.
cc
The GLSL 1.20 variant must stay in plasma/ambient's packs: Qt's OpenGL
RHI selects 120/130/140 on desktop GL <= 3.1 and the renderer tier does
not exclude that hardware, so dropping it (the previous attempt) was a
regression for old GL — caught in review. qsb 6.8 cannot lower those
shaders' uint hash constants for 1.20 while 6.9+ can, and the committed
.qsb were baked with 6.11: the bake toolchain is therefore pinned apart
from the 6.8 runtime floor (vars.QT_VERSION_SHADERTOOLS, default 6.9.3).
The gate also passes -b only to vertex shaders without the no-batch
marker, exactly like build.rs.
cc
PlaybackEngine::AlsaDirect and StreamType::AlsaDirect become Direct, carrying
Arc<dyn DirectSink> instead of Arc<AlsaDirectStream>. Eleven match arms and
seven sites keep working untouched because they only ever called trait
methods; duplicating them per platform would have been the alternative.
CORRECTION to e61cd7bd8's message: it claimed the ALSA path still
monomorphises to static dispatch. That was true when written and is NOT true
now - the payload is a trait object, so those calls are virtual. Measured
where it lands: write_f32 runs once per 8192-frame chunk, i.e. 5.4 indirect
calls/second at 44.1 kHz and 23.4 at 192 kHz, each immediately entering
blocking device I/O. channels() once at construction, sample_rate/stop/drain
at boundaries. Nothing in the sample path.
Two things Windows CANNOT compile-check, found by reading rather than by the
compiler, and both would have broken the Linux build:
- Four cfg(linux) arms passed a matched Arc<dyn DirectSink> to
new_alsa_direct, which takes the concrete type. Arc unsizes one way only.
- set_hardware_volume is called inside a cfg(linux) block and was not on the
trait. It is now, with a default that REFUSES rather than silently
succeeding, overridden by ALSA on Linux only - a sink with no mixer must
not pretend it moved the volume.
StreamType::Direct is also no longer cfg(linux). Gating it was what stopped
this compiler from checking any arm that handles it, which is the exact blind
spot this port has to work around; the variant now compiles everywhere and
Windows will refuse anything I get wrong in it.
The log prefix moved onto the sink (DirectSink::log_label) rather than being
passed in: four call sites hard-coding "ALSA Direct Engine" is four chances to
mislabel a WASAPI stream. play/pause/stop/set_volume use it too - previously
only the writer thread did, so a WASAPI stream would have logged ALSA
lifecycle lines.
Sol reviewed as the Linux compiler before this commit and found no Linux type
error; the stale references and hard-coded labels above are its findings.
cc
Window.topPadding..rightPadding and the SafeArea attached type are Qt
6.9; on the Linux pin (6.8 LTS) declaring them on the ApplicationWindow
refuses the whole component at Main.qml:173 ("Cannot assign to
non-existent property"), measured on the CI VM 2026-08-25 and on a Pi 4
with Debian 13's Qt 6.8.2. The paddings are now set imperatively from a
Loader active only on macOS, and HeaderBar reads SafeArea.margins.left
through a macOS-only Loader into a plain property, so no binding names
the type on other platforms. Behaviour on macOS is unchanged.
cc
ALSA and WASAPI need byte-identical behaviour in the writer thread - same
chunking, same gapless hand-off, same drain - and two copies of 112 lines
drift. `qbz_audio::backend::DirectSink` names the five methods the engine
already called on AlsaDirectStream, both streams forward to their inherent
methods with no wrapping, and `alsa_writer_thread` becomes
`direct_writer_thread<S: DirectSink>` plus a log label.
Linux is unchanged: the ALSA call site passes its old label verbatim, so
every emitted string is identical, and `S` infers to AlsaDirectStream, so the
call monomorphises to the same static dispatch it had - no trait object, no
vtable in the audio path.
Sol reviewed this before the commit, per the rule added today, and found the
defect that mattered:
WasapiDirectStream::drain only SENT Ctl::Drain and returned immediately,
while the trait - and the ALSA drain it now shares a thread with - promises
to play the queue out first. Worse, the render thread EXITED after draining,
so a later append would have written into a dead thread. Gapless on Windows
would have been broken in a way no unit test reaches.
drain now blocks on a reply from the render thread, and the render thread
answers and KEEPS RUNNING. Proven with the example: drain returns Ok in 28 ms
rather than 0, and a write after it succeeds. Starvation is also counted as an
EVENT now rather than per period - an idle stream otherwise reported hundreds
of underruns for one real one.
Sol also caught two stale doc references to the old function name and a
"draining ALSA buffer" line that a WASAPI stream would have printed.
cc
qbz-qt pulls wayland-sys through arboard's wayland-data-control, and its
build script wants wayland-client.pc. The CI VM had the Slint-era dev
libraries baked in, which hid the gap; the first real qt-gate run on
pre-release exposed it. Added to test-crates' qt-gate and to
build-qt-linux.
cc
The parenthetical added in 1a01abde6 says `$null` removes the variable. It
does not, for the call the comment is about: PowerShell coerces `$null` to
String.Empty when binding a `string` parameter, so
`[Environment]::SetEnvironmentVariable('X', $null, 'Process')` leaves the name
PRESENT and empty, and a child process inherits it that way. That is the call
that produced the original observation.
`$env:X = $null` IS the one that removes it, so both spellings belong in the
comment rather than one standing for the other.
Measured on Windows 11, PowerShell 7.6.5, with a child that enumerates its own
environment rather than using cmd's `if defined` - which collapses "empty" and
"absent" and is what made an earlier reading of this look the other way:
[Environment]::SetEnvironmentVariable('X', $null, 'Process')
python -c "import os; print('X' in os.environ, repr(os.environ.get('X')))"
-> True ''
never set at all
-> False
The POSIX half of the comment is unchanged and was already correct: `VAR= cmd`
delivers `Some("")`, verified the same way.
Comment only; no behaviour change.
cc
The render thread and the handle. COM objects are !Send, so one thread owns
every one of them for the life of the stream and this struct is a bounded
channel plus atomics - the same back-pressure snd_pcm_writei gives the ALSA
path for free.
Opens by ENDPOINT ID, never by friendly name: Windows gives several
endpoints the same name, which is what hid this DAC from the picker until
249adc77b. Walks the format ladder, pre-aligns the period to 128 bytes,
keeps the documented NOT_ALIGNED recovery for the devices that still need
it, joins MMCSS Pro Audio, and refuses S_FALSE loudly - a "closest match" is
by definition not the format we asked for.
Verified against the owner's Cambridge Audio USB Audio 2.0 with the new
example, and it matches the spike exactly:
rate 96000, S24In32 (24/32), period 30000 hns, buffer 288 frames,
realigned false, timing Events, mode DirectHardware,
960000 frames, 0 underruns, 0 event timeouts
Three faults were found on the way, and all three were in the MEASUREMENT
rather than the audio - worth recording because each looked like a hardware
problem:
- 339 "underruns" were event timeouts counted in the same counter. Split
them; they are different faults and were hiding each other.
- 337 event timeouts came from a 2x-period deadline. At a 3 ms period that
expired on 10% of wakeups because Windows scheduling is not that
punctual. The deadline exists to stop the thread hanging on a dead
device, not to detect glitches, and the spike waited 1000 ms and saw
none in ten minutes. It is generous now.
- The last 2 were a drain's trailing silence. A drain emitting silence is
the drain working.
The stub for every other platform mirrors alsa_direct's: constructors refuse,
teardown succeeds, so a caller written against this compiles and unwinds
everywhere. `cargo tree` shows exactly one windows v0.62.2 - no second
windows tree entered the graph.
cc
Reviewed on Linux and Windows; the focused renderer tests pass. The two failing Qt checks are pre-existing CI infrastructure failures: Qt 6.8 qsb cannot bake ambient/plasma GLSL 120, and the runner lacks libwayland-client development files.
cx
Format ladder, PCM packing and period arithmetic for phase B, with the tests
that make them provable. No COM yet: this half compiles and is tested on
every host, which matters because it is where the platform-specific mistakes
live.
The one that bites: Windows 24-in-32 is LEFT-aligned, the 24 valid bits in
the HIGH bytes, where ALSA's S24_LE is right-aligned. Porting the ALSA
packing verbatim shifts every sample down 8 bits and plays 48 dB QUIET, which
sounds like a volume bug and survives a listening test. There is a test whose
only job is to fail if that ever happens.
Scaling matches alsa_direct exactly (8_388_607 and 32767, not the power of
two), so the two backends cannot disagree about full scale. The plan's test
sketch asserted i16::MIN and -8_388_608; those follow from the OTHER scaling
choice, so the tests assert the behaviour this file actually has.
aligned_period_hns rounds the period so frames * block_align is a whole
number of 128-byte blocks, which is how AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED
stops firing rather than being recovered from. Verified against the two
periods measured on the owner's DAC: 3 ms at 192k 24-in-32 is already
aligned at 576 frames and must not move.
LADDER order comes from research/01; the spike found that this DAC accepts
only S24In32, so the ladder is one rung deep there. It stays a ladder because
other devices are not.
cc
The mode LED is derived from SETTINGS (output_labels.rs reads
audio.exclusive_mode and the ALSA plugin), so it reports what was ASKED FOR.
Nothing reported what the opened stream actually negotiated, which means
"exclusive worked" and "we asked for exclusive" were indistinguishable - and
that distinction is what phase B's acceptance turns on.
PlaybackEvent already carries bit_perfect_mode, so this is a publish, not new
plumbing: the poll tick maps it to direct | plugin | disabled | unknown and
set_effective_stream dedupes on it alongside the delivered params, so a
steady stream still costs no Qt-thread hop. The quality tooltip appends
"stream: <mode>" on both of its paths; unknown and empty print nothing.
Four technical tokens, so no new translatable string.
cc
Qt's default rasteriser on Windows renders text thin and jagged. The owner's
words for the lyrics panel were "como fuente de bloc de notas con extra
alias", and the supplied screenshot shows exactly that: broken strokes and
visible stair-stepping on every line.
It is the RASTERISER, not the font and not the lyrics view. Verified on the
box by launching the same build with
QT_QPA_PLATFORM=windows:fontengine=freetype and comparing the same panel:
identical font, identical sizes, clean glyphs. So the fix belongs at the
application level, where it also cleans up the rest of the UI.
Applied only when the variable carries no real value. `offscreen` is what
the release smoke gate passes here and overwriting it would make the gate
test a platform nobody ships. Empty counts as absent for the same reason the
renderer preference now does: a cleared variable is present-with-no-value,
which var_os().is_some() reports as set.
cc
apply_renderer_preference gated on var_os("QSG_RHI_BACKEND").is_some(), which
answers YES for a variable that is present with no value. Clearing a variable
rather than unsetting it is the common way to end up there - `QSG_RHI_BACKEND=
./qbz` in a shell, and on Windows PowerShell's SetEnvironmentVariable(name,
$null), which leaves the name present and empty. The whole preference then
stopped applying, the user's persisted Settings choice included, and the only
clue was one info line claiming an explicit backend was present.
Found on the Windows box, where a test harness cleared the variable that way
and the renderer arm silently never ran. It reproduces on Linux the same way;
this is not a platform fix.
QBZ_RENDERER, read a few lines below, has always filtered the empty string.
This applies the same rule to the two Qt variables. The predicate is split
from the lookup so the three new tests exercise it without mutating
process-global env, which would race the other tests in the binary.
cc
A regression from 8baf68db8, reported from the running app: the album and
artist header gradients stopped appearing on Windows.
Six sites undid a file:// URL by hand with strip_prefix/trim_start_matches.
Before the URL builder was fixed they worked BY ACCIDENT: file_url produced
the broken "file://C:\Users\x", and stripping seven characters off that left
a valid Windows path. With the URL correct at "file:///C:/Users/x" the same
strip yields "/C:/Users/x", which opens nothing - so the fix to the QML-
facing half broke the Rust-facing half everywhere the inverse was open-coded.
atmosphere_qt.rs is the one that showed: it decodes the cover to build the
128x128 field the header gradient IS, so a failed open means no gradient at
all, with nothing logged. album_qt.rs feeds the same pipeline plus the flat
tint, ambient_qt.rs extracts the colour triad, artwork_qt.rs uses the string
as BOTH a cache key and a path, myqbz_detail_qt.rs hands a bare path to the
artwork channel, and qbz-source returns an ArtRef::File.
local_path also undoes the three percent-escapes these sites never did, so a
cover under a path holding # ? or % now opens on every platform - previously
it failed on all of them. The two sites that hash the string will miss their
cache once and regenerate.
Three strips deliberately stay: notify.rs is cfg(linux, macos); the
playlist-import one takes a filename STEM, where a leading slash cannot
matter; and qbz-theme parses a COSMIC desktop config Windows never reads.
The plan's step-8 grep could not have caught these - it searches for
starts_with('/') and format!("file://, not for the inverse.
cc
Windows endpoint friendly names are not unique. Measured on this box, FOUR
active render endpoints resolved to three display strings, and two were both
called "Altavoces": the onboard Realtek and a Cambridge Audio USB DAC.
device_filter dedups on the display, so the DAC was folded away and could
not be selected at all - and a DAC you cannot select cannot be sent a
bit-perfect stream, so this also blocked phase B.
device_filter.rs:20-23 states the opposite in a comment - "macOS/Windows
device names carry unique displays, so they pass through untouched". That is
the third comment in this tree asserting a mechanism that does not hold.
Nothing new is queried: cpal 0.17.3 already fills the fields this backend
ignored. name() is the ENDPOINT and repeats; driver() is the ADAPTER and is
what tells them apart; id() is the stable WASAPI endpoint GUID. On Windows
the id becomes the GUID, the display becomes "endpoint (adapter)", and the
adapter lands in the description field that was hard-coded None. The default
device is matched by id there too, since three endpoints shared a name.
Additive cfg arms only, per the protected-core rule: every other platform
takes the same (name, name, None, name == default_name) tuple it took
before, byte for byte. The open path keeps the name comparison as a fallback
so a device_id saved before this change still resolves.
Verified against the real hardware, before and after, with a standalone cpal
probe: the DAC goes from DROPPED to listed, labelled and flagged default.
cc
Adds isWindows, traySupported, systemNotificationsSupported and
rendererSelectable to the settings document, so Settings QML keeps reading
ONE document the way it already does for isMacos. The two any(...) lists
flip to include Windows when plan A-2 T3/T5 land; they point at that line.
Then the Audio tab, measured on the box with the tab open:
HiFi Wizard was live on Windows. qbz-dac-wizard-core generates a
PipeWire/WirePlumber config and knows no other stack, so the wizard cannot
finish anywhere but Linux. Hidden, not disabled - a disabled row invites a
bug report. This is not the reference's backend-is-pipewire gate returning:
inside Linux every backend still sees it, per the 2026-08-11 ruling in that
file's own comment.
Reserve DAC is D-Bus org.freedesktop.ReserveDevice1, Linux only. On Windows
exclusive mode IS the reservation; on macOS it is Hog Mode.
Exclusive mode was visible with no backend able to honour it. It now follows
the backend: Linux unchanged (always visible, enabled only on ALSA), hidden
on macOS, and on Windows it appears when Phase B publishes backendIsWasapi.
QML reads an absent doc property as undefined and undefined === true is
false, so this is correct today and correct again the day the field lands.
The DSD row follows the same pair.
Main.qml no longer boots the wizard off Linux, where its row is hidden and
the audio-stack probe would reach nothing.
Linux sees no change: every gate is isLinux-true or an added || on a field
that is undefined there. Five QML audits pass.
cc
winresource, not winres: winres has been unmaintained since 2021 and this
is its maintained fork. Gated to cfg(windows) as a target build-dependency,
so no other platform resolves it at all.
The manifest declares longPathAware and NOTHING else. Qt 6 sets
PerMonitorV2 itself at startup and a manifest DPI declaration is immutable,
so the two together either log a permanent COM error 0x5 or degrade
scaling - Qt owns DPI awareness.
The .ico carries 256/128/64/48/32. The four small entries are BMP and only
256 is PNG: measured on this box, System.Drawing.Icon (GDI+) throws
ArgumentOutOfRangeException in ToBitmap() on a PNG-payload entry, while WIC
decodes it. 256 has to be PNG because the directory encodes its size as 0.
Pixels verified against the source PNG at four points including an
asymmetric one, which is what proves the BGRA order and the bottom-up flip.
cc
Three copies read HOSTNAME and then /etc/hostname. Windows has neither, so
every Windows install fell through to the same last-resort word and they
collided on Qobuz Connect. One implementation in qbz-app; HOSTNAME keeps
precedence so Linux behaviour does not move.
qbzd/src/qconnect/transport.rs was a fourth copy the plan did not list, and
the settings_qt.rs one it did list no longer exists on this branch. Both
callers keep their own last-resort word - "Desktop" here, and qbzd's own -
because they are user-visible and mean different things.
qbz-credentials/src/lib.rs:273 also reads HOSTNAME and is deliberately NOT
touched: it is a salt in the KDF fallback chain, not a device label, so
adding COMPUTERNAME there would change the derived key on Windows and make
every already-encrypted credential file undecryptable. That is W17.
cc
Windows was neither is_macos nor is_linux, so QML could not name it and
every two-way platform ternary handed it the Linux arm by default.
Compile-time constant like the other two.
This is infrastructure, not a fix: the audit below found NO site that
currently resolves wrong on Windows. It is the property the settings
per-platform work and the A-2 chrome slice branch on.
Every two-way platform branch, and what Windows gets today:
Main.qml:145 chrome flags -> Qt.Window. default_use_system_title_bar
is !cfg!(macos), so systemTitleBar is
true on Windows and the ternary never
reaches the macOS/Linux arms. With the
title bar off it takes FramelessWindowHint,
which is also the right arm on Windows -
ExpandedClientAreaHint paints nothing there
Main.qml:241 macOS diagnostic -> skipped
Main.qml:435 macChromeTimer -> not running
Main.qml:640 traffic-light inset -> skipped
ImmersiveHeader.qml:42 -> no macOS inset
HeaderBar.qml:89 drawn cluster -> gated on showWindowControls, which the
native title bar leaves off; only the
native buttons are drawn (confirmed on screen)
HeaderBar.qml:104 -> no macOS inset
AppearanceSettings.qml:493 -> hidden (useSystemTitleBar is true)
AppearanceSettings.qml:510,521 -> visible; both are meaningful on Windows
and 521 is the route to custom chrome
AppearanceSettings.qml:583,603 -> hidden
AppearanceSettings.qml:630-646 -> renderer group hidden
hotkeys_qt.rs:423,961 -> Ctrl glyphs
main.rs:3099-3102 -> has its own Windows arm
renderer_qt.rs:420 -> gpuSelectable false, GPU row hidden
settings_qt.rs:469 -> system title bar on by default
settings_qt.rs:2258 -> is_macos false
cc
Adversarial review of the previous commit found four defects, three of them
mine and one of them fatal to the Local Library on Windows.
fs::canonicalize RETURNS extended-length syntax on Windows and
qbz-library/src/scan.rs stores that string for every scanned file, so the
helper saw \?\C:\Music\cover.jpg, read it as a UNC path whose server was
"?", and emitted file://%3F/C:/Music/... - every local cover, gone. Both
verbatim spellings are now undone, \?\UNC\ included.
local_path dropped the // that a UNC authority stands for, so the round trip
returned nas/music/x, a path that names nothing. It now returns
//nas/music/x, which is also what QUrl::toLocalFile gives back.
file_url pushed a third slash for any path that did not start with one, so a
relative artwork_path changed from file://covers/x to file:///covers/x on
Linux. Neither resolves - a relative path is not a valid file:// URL - but
Linux behaviour does not move in a Windows port. The extra slash is now
emitted only for a drive path.
is_local_abs_path now requires a separator after the colon, so C:cover.jpg
is drive-RELATIVE and no longer claimed as absolute on either platform.
The two residual ambiguities - a Unix path whose first component is a single
letter plus a colon - are documented at the top of the module rather than
papered over: no string algorithm separates them, and deciding by host would
put the failure back on the machine that ships.
Four regression tests added, one per defect.
cc
Seven sites built format!("file://{p}") by hand or tested starts_with('/')
for "absolute path". Both are Unix-only: file://C:/... parses the drive
letter as the URL HOST, and C:\... does not start with '/'. On Windows
that silently cost every album cover, every runtime-tinted icon and the
local folder tree - a URL that resolves to nothing renders as an empty
rectangle, not as an error, so nothing complained.
qbz_models::fs_url now owns file_url / local_path / is_local_abs_path,
deciding by path SHAPE (POSIX, drive, UNC) rather than by host, so the
tests run and fail everywhere rather than only on Windows.
Linux behaviour is unchanged, deliberately: the backslash-to-slash rewrite
fires only for a Windows-shaped input, because a backslash is a legal
filename character on Unix and rewriting it would point /home/v/AC\DC at
a directory that does not exist. There is a test for exactly that.
Three of the seven sites were not in the plan's list and were found by its
step 8 grep: local_ephemeral.rs, local_playlist_qt.rs, playback_qt.rs.
Two starts_with('/') hits stay and are not local-path tests:
qbz-library/src/mount_info.rs is cfg(linux) mount-point arithmetic, and
qbz-theme's is parsing a COSMIC desktop config that Windows never reads.
cc
The committed .qsb are binary but carry raw CR bytes, so a future blanket
`* text` would mangle them. Everything else is pinned to LF so a checkout
with autocrlf on cannot turn 2.1.0 into 2.1.0<CR> in a version grep, or
CRLF the GLSL and QML sources.
.rs is pinned too, which the plan's list omitted: it is the largest text
set in the tree and the same class of invisible breakage applies.
git add --renormalize . stages nothing, so no file changes today.
cc
windows_subsystem was declared nowhere, so the app carried a console
window on Windows. The file logger is unaffected; only the inherited
stdio console goes.
The offscreen smoke gate has not run against this yet. If smoke.log comes
back empty the fix is AttachConsole(ATTACH_PARENT_PROCESS) behind
cfg(windows), not reverting this; there is a note at the attribute saying
so.
cc
The gl/gles/femtovg pref forced QSG_RHI_BACKEND=opengl on every non-macOS
host, dragging Windows off its healthy d3d11 default onto desktop GL. That
is a downgrade, not the GPU tier the pref means, so Windows takes the same
remap macOS already had.
And with no explicit style Qt loads Windows on Windows and Fusion on
Linux, never Basic, so the popups without an explicit background inherit
native chrome. Pinned before QGuiApplication, since it must run before the
first QtQuick.Controls import, and windeployqt then ships one style.
cc
souvlaki's Windows backend does config.hwnd.expect() at
platform/windows/mod.rs:57, so the None we passed was a panic inside a
tokio task on the first session, not the Err the match was written for.
The comment on that field claimed init would merely fail; it was read from
the comment, not the mechanism. No HWND is wired yet, so spawn() bows out
on Windows and the body moves to spawn_native, which is compiled out
there. Phase A ships without media keys and says so.
The offline-folder opener spawned xdg-open on every non-macOS host,
Windows included, where it does not exist. Route it through the open crate
like the other eleven sites in this crate; Linux and macOS reach the same
two binaries as before.
cc
lock.rs imported std::os::unix and called libc::flock unconditionally, so
a workspace test run could not compile on Windows.
The Windows arm locks ONE byte at offset 1 GiB rather than the whole file.
Byte-range locks are mandatory there, not advisory like flock: measured on
this box, a whole-file lock from offset 0 makes the losing instance's
read_to_string fail with ERROR_LOCK_VIOLATION, so AlreadyRunning would
always carry None - the pid is the only reason that variant exists. With
the lock byte past any content the pid still reads back, the same handle
conflict is still detected, and the lock still releases when the File
drops. Reserving a region past EOF is what SQLite's Windows VFS does.
Only ERROR_LOCK_VIOLATION means contention; a filesystem that does not
implement byte-range locks answers ERROR_INVALID_FUNCTION and must surface
as Io, not as a daemon that is not running. The unix arm keeps its
existing any-failure-means-busy behaviour unchanged.
cc
The not(unix) arm returned Err and both preflight() and prepare()
propagate it, so the catalog never bootstrapped on Windows and the Local
Library died at preflight.
GetDiskFreeSpaceExW on the nearest existing ancestor, since the catalog
root may not exist on first run; LockFileEx with FAIL_IMMEDIATELY, mapping
ERROR_LOCK_VIOLATION to BootstrapBusy like the flock arm and any other
error to Io. Nothing reads this lock file's contents, so the whole-file
range is safe here.
cc
qbz-credentials still asked for the Linux features on every target with
default_features on. keyring 3 has no default backend: without the
per-platform feature it exports the in-memory mock, whose writes succeed
and whose contents die with the process, so the offline-cache master key
was regenerated every launch and every wrapped blob failed AES-GCM on the
next start, with the logs reporting a working keyring. It survived only
because Cargo unified features with qbz-secrets, which is the fragility
the per-target block exists to remove.
Also refuse a non-disk-backed backend at runtime and take the KDF path.
Measured in keyring-3.6.3, not assumed: windows.rs, macos.rs and
secret_service.rs all inherit the trait default UntilDelete
(credential.rs:171); mock.rs:228 overrides it to EntryOnly. Testing for
ProcessOnly, as first drafted, would never have fired.
cc
Neither cc-rs nor cxx-qt-build passes them, and without both nothing that
includes a Qt header compiles on Windows.
/Zc:__cplusplus: MSVC reports __cplusplus as 199711L whatever /std: says,
and qcompilerdetection.h:1317 rejects that value outright on every
translation unit, ours and moc's alike.
/permissive-: measured on Qt 6.9.3, the headers do not parse in MSVC's
default mode - qtmochelpers.h:262 C2065 'result', qcomparehelpers.h:1348
C2968 recursive alias, and C2737 on every moc staticMetaObject.
Applied to both cc builders: this crate's own, which compiles the nine RHI
items, and cxx-qt's. Patching only the latter changed nothing, because the
failure came from the former. Gated on the target env, so the Linux and
macOS builds cannot see either flag.
cc
qt-build-utils writes the collected path verbatim as the qrc alias, and
read_dir joins with a backslash on Windows, so none of the 1469 aliases
resolved: 1106 icons, 9 fonts and 21 shaders would each fail with
"Cannot open". Normalise only when the build host is Windows, since a
backslash is a legal filename character on Unix.
Also probe qsb.exe and QT_ROOT_DIR/bin, because Path::is_file never
appends .exe, and fix the SpectrumBand hand-bake comment, whose command
lacked --hlsl 50 and the GLES variants that build.rs actually passes.
cc
Preserve Afonso’s restart regression test while selecting native stores only on Linux, macOS and Windows. Unsupported targets take the persisted KDF path instead of keyring’s successful in-memory mock; the stale workflow change is deliberately excluded.
cx
Keep official upstream releases and Gatekeeper guidance as the primary route. Present Afonso’s notarized Homebrew cask and DMGs as an independent community alternative with explicit provenance and endorsement boundaries.
cx
Preserve Afonso’s same-directory temp-file replacement on top of the configurable cache root from #708. Cover the complete explicit-root round trip and document the exact process-crash and concurrent-writer guarantees.
cx
Measured cold on the 4-vCPU CI VM: CARGO_BUILD_JOBS=4 finishes the
workspace test in 392 s against 589 s at jobs=2, with a 2.7 GB peak on
a 10 GB guest. The throttle protected a 30 GB rustc that no longer
exists in this graph. Leave the job count to cargo in the workflow and
in scripts/cargo-test.sh alike; --no-fail-fast and the 20-minute limit
stay.
cc
The single-instance lock is a D-Bus well-known name, so on a developer
box with QBZ open the debug binary presented the running instance and
exited with one log line, failing the gate for the wrong reason. The
smoke now runs under dbus-run-session with its own XDG dirs — never the
developer's config or session — in the script and the workflow alike.
cc
Per the 2026-08-26 rule: the workflow's test job runs scripts/cargo-test.sh
(CARGO_BUILD_JOBS=2, --no-fail-fast, 20-minute limit), the script
documents what --workspace covers now that the Slint crates are gone,
and QT=1 runs the Qt gates locally in the same order as the shader-gate
and qt-gate jobs: audits, shader bake, Slint-free graph, cargo test
-p qbz-qt, offscreen boot.
cc
The previous README described the Slint build: the memory wall, the
slint-*.sh dev loops, nightly + mold, a Debian dependency list without
Qt, seven locales, the wgpu/femtovg renderer table. All of it is
replaced with the Qt reality: cxx-qt, Qt 6.8 floor with private
headers, scripts/qt-run.sh, the measured build times, the real
QBZ_RENDERER semantics. Adds what shipped since: Jellyfin, Subsonic,
CD/SACD playback and ripping, the metadata editor, the playlist
manager. Adds a table of contents, a "Reporting a problem" section
(Settings > Share logs), the LLM/AI tools statement, the multichannel
DSD known issue, and corrects the Flatpak bit-perfect note (it works;
one device-reservation grant, shown in Settings > Flatpak).
cc
qml_resolution_audit hard-coded /usr/lib64/qt6/qml, so on a runner it
saw zero Qt types and reported every Rectangle as unresolved; it now
takes QBZ_QT_QML_DIR (authoritative), then QT_ROOT_DIR, then the distro
layouts, and fails loudly when none has plugins.qmltypes. test-crates
runs the audits after the Qt install, pointed at it.
build.rs re-bakes a shader only when its .frag is newer than its .qsb,
which makes a fresh checkout nondeterministic whenever qsb is on PATH.
Release and test builds now install Qt without qtshadertools, so the
committed .qsb ship as-is, and a new shader-gate job bakes every shader
to a temp dir with the pinned qsb using build.rs's own target lists
(read from the file, so they cannot drift). It is red today at 6.8.3:
plasma.frag's uint literals do not bake for the GLSL 120 target with
qsb 6.8 (6.9+ accept them) — that is the finding, not a false positive.
cc
test-crates is meant to merge first, ahead of the release-workflow
rewrites; a gate over .github/workflows/ would fail on the still-Slint
release files until those land. The gate stays in release-linux and
release-linux-aarch64, which are the files it protects.
cc
The Slint crates are gone from the workspace, so cargo-test.sh's four
--exclude flags were stale; qbz-qt is the only exclusion now (it is the
one crate that needs Qt, and the sole consumer of wayland-sys — the
reason the workspace test fails on a runner without libwayland-dev).
Release workflows write their caches from main only: nothing but
test-crates.yml is meant to run from pre-release.
cc
build-aarch64-linux.sh builds -p qbz-qt natively with the distro's Qt
(gated at >= 6.8, the tree's floor; Debian 13 / Ubuntu 25.04 / Fedora
41 qualify), installs rustup when absent, forces one rustc at a time on
<= 4 GB boxes, and runs the same offscreen smoke as CI. Its CROSS mode
is retired honestly: cxx-qt runs the target Qt's qmake during the
build, which cross-rs's Ubuntu 20.04 images cannot do. The qbzd script
and Cross.toml keep cross-compiling the daemon, with the Qt-free dep
set.
cc