Blog

Solid Lines Around Hardware

My network widget broke today because a property changed underneath it. More precisely, the widget knew enough about the hardware backend to parse a backend-specific property, and not enough to notice when that property was absent. This is the sort of knowledge that makes software feel powerful right up to the first different machine.

Solid is the KDE 4 attempt to draw a stable line between applications and hardware discovery mechanisms. An application should ask for devices and capabilities in KDE terms. A backend can obtain those facts from HAL, NetworkManager, or another platform facility. The abstraction is not meant to make all hardware identical; it is meant to stop every application from inventing a private translation layer.

The useful mental model is a device plus interfaces. A device has an identity and general properties. Interfaces describe capabilities such as storage access, battery status, or networking. Code can first ask whether a capability exists, then use the corresponding interface:

Solid::Device device(udi);

if (device.isDeviceInterface(Solid::DeviceInterface::StorageVolume)) {
    Solid::StorageVolume *volume =
        device.as<Solid::StorageVolume>();
    qDebug() << volume->label() << volume->size();
}

The pointer still needs checking because devices disappear and backends can fail. A USB disk is not contractually obliged to remain attached while my slot admires it.

Identity is harder than it first appears. A display name is for humans and can change. A device path may reflect discovery order. A HAL UDI is useful within that backend but should not leak into an application’s persistent file format if the application claims backend independence. For a short-lived object map, the current unique identifier is fine. For durable preferences, I need a stable property appropriate to the device class, and sometimes no truly stable identifier exists.

Hotplug events introduce ordering questions. My first implementation reacted to a “device added” signal by immediately requesting every interface. Some properties were not ready yet, and parent devices could appear after children. The safer path is to tolerate incomplete devices, query only required capabilities, and update when relevant properties change. Applications must not infer a complete topology from the first signal they see.

Networking demonstrates why policy should remain separate. NetworkManager can report devices, available networks, activation state, and changes. Solid can present that information consistently to KDE code. It should not mean that a text editor decides which wireless network to activate. A desktop networking component may own that policy; most applications only need to know whether connectivity is currently available, perhaps with enough uncertainty to avoid promising too much.

Storage has a similar distinction. Discovering a volume is not the same as mounting it, and mounting it is not the same as opening a file manager. Those are three mechanisms with three possible policy owners. When a camera appears, a photo application may offer an import action. Automatically performing one because a hardware signal arrived would be efficient in the same way as a trapdoor.

For the widget, I replaced direct HAL property access with a tiny internal state derived from Solid interfaces:

struct NetworkState
{
    QString name;
    bool available;
    bool active;
};

The widget receives this state and paints it. It no longer knows whether the information came from NetworkManager or a test backend. The adapter listens for device and property changes, rebuilds the affected state, and emits one application-level update. This also keeps backend callbacks away from paint code.

I prefer capability queries over a deep inheritance tree of device types. Real hardware combines roles inconveniently, and capability composition lets one object expose several useful interfaces. The caveat is discoverability: callers must know which combinations make sense, and an abstraction can become a bag of optional pointers if its contracts are weak.

I also prefer asynchronous operations for actions such as mounting or network activation. Hardware can wait for media, authentication, or a helper process. Blocking the GUI thread while reality decides what to do is poor manners. Completion needs an error path that preserves enough backend detail for diagnosis without requiring the application to parse backend internals.

None of this makes Solid finished. We still need to learn which properties are genuinely portable, how device lifetimes should be represented, and how much backend-specific information may escape without defeating the purpose. HAL itself is evolving, and NetworkManager behaviour differs across the machines I can test. A clean interface can expose uncertainty; it cannot abolish it.

My practical rule after today’s failure is simple: application code depends on capabilities, UI code depends on application state, and raw backend properties stop at the adapter boundary. Exceptions may be necessary for specialised tools, but they should look like exceptions rather than quietly becoming architecture.

The widget now survives the missing property. It also reports my wired interface as “network cable,” which is accurate, stable, and not likely to win a naming award.

Two Roads to Composited Windows

I enabled Compiz on a test machine today and immediately lost window borders. The cube worked, naturally. A desktop may be unusable, but at least it can be unusable on six rotating faces.

The experiment clarified three pieces that are often bundled together in conversation. Compiz is the compositing window manager. It asks the X server for redirected window contents, combines those textures, and draws the resulting scene. Xgl is an X server built on top of OpenGL. AIGLX takes a different route by adding accelerated indirect GL rendering to the conventional X server and driver path.

That difference affects setup and failure modes. With Xgl I start a separate server and run the session inside it. With AIGLX I keep the normal server architecture but depend on suitable server and driver support. Neither route makes every driver correct by declaration.

For diagnosis I first remove decoration and plugins from the mystery. I run a plain window manager, confirm direct rendering information, then add composition. Useful checks include:

glxinfo | grep direct
xdpyinfo | grep -i composite
compiz --replace gconf decoration move resize

The exact plugin names and options are changing, so this is a notebook entry rather than scripture. The important mechanism is staged testing: server extension, GL path, window manager, then decorator.

My missing borders came from the decorator not running, not from texture rendering. Starting it restored frames, though resizing still exposed corruption with this driver.

I prefer AIGLX’s direction because it preserves more of the existing X server arrangement and avoids nesting a whole desktop on another server. The caveat is substantial: today Xgl may work better on a particular proprietary driver, while AIGLX support and performance vary. Architecture preferences do not repair hardware support.

Before comparing speed, I also verify that both tests use the same resolution, colour depth, effects, and driver. Otherwise the benchmark measures configuration drift with impressive precision.

These experiments are useful for KDE 4 because they reveal what a composited desktop might rely on, not because spinning windows constitute a completed desktop design. Effects must degrade gracefully, input must remain correct, and ordinary two-dimensional work must not become collateral damage. I have disabled the cube for now. It was beginning to look smug.

Phonon Is Not a Codec Collection

A test player handled one Ogg file and failed on another machine before lunch. My first reaction was to ask which Phonon codec was missing. That question contains the bug in my mental model.

Phonon is intended as a multimedia API above actual playback engines. The application builds a graph of media objects, audio outputs, and paths. A backend translates that graph to whatever engine is available. Codec support therefore depends on the backend and its installed components, not on Phonon carrying a suitcase of decoders.

The small playback case looks roughly like this:

Phonon::MediaObject *media = new Phonon::MediaObject(this);
Phonon::AudioOutput *output =
    new Phonon::AudioOutput(Phonon::MusicCategory, this);

Phonon::createPath(media, output);
media->setCurrentSource(Phonon::MediaSource(fileName));
media->play();

The category matters. It describes the purpose of the sound so system policy can distinguish music from notifications or communication. It is not merely a friendly enum to satisfy the compiler.

Errors remain asynchronous. Calling play() starts a process; it does not certify that bytes will reach speakers. The application must listen for state changes and present the backend’s error without pretending every failure is a missing file.

I prefer depending on a narrow playback abstraction instead of teaching each KDE application the details of several multimedia engines. That should make device selection and policy consistent. The caveat is the least-common-denominator problem: specialised editors and unusual pipelines may need capabilities the abstraction cannot sensibly expose.

I am also avoiding backend detection in the application. Branching on a backend name would make today’s workaround tomorrow’s required behaviour. If a capability is necessary, the API should expose that capability or the application should report that it cannot perform the operation.

For porting, I am keeping the boundary obvious. The document remembers a URL and playback position; a small controller owns Phonon objects; widgets issue commands and display state. This makes backend surprises testable without turning the main window into an audio engine wearing buttons.

The architecture is still settling, and backend behaviour will not be perfectly identical. Today’s useful certainty is modest: “works through one engine here” is not the same claim as “Phonon supports this format everywhere.” Computers remain attentive readers of fine print.

ThreadWeaver and the Size of a Job

I fed 12,000 thumbnail jobs into a ThreadWeaver experiment today. The queue accepted them with admirable composure. The application then spent more time managing tiny jobs than producing thumbnails.

The mechanism is straightforward: jobs enter a queue, worker threads take runnable jobs, and completion is reported back. This is much better than creating one thread per thumbnail. Threads have stacks, scheduling cost, and an unfortunate tendency to outlive the code that launched them.

My mistake was choosing a job that was too small. Each item performed a little path work, checked a cache, and often returned without decoding anything. Queue locking and signal delivery became a noticeable fraction of total work.

Batching nearby items helped:

class ThumbnailBatch : public ThreadWeaver::Job
{
public:
    ThumbnailBatch(const QStringList &paths)
        : m_paths(paths)
    {
    }

protected:
    void run()
    {
        foreach (const QString &path, m_paths)
            createThumbnail(path);
    }

private:
    QStringList m_paths;
};

The batch is large enough to amortise scheduling but small enough that several workers remain busy. There is no universal number. Disk latency, decoding cost, and cache hit rate all change the useful grain size, so I am measuring rather than engraving 32 on a tablet.

Cancellation also becomes a design issue. Removing queued jobs is easy; stopping one already decoding is cooperative. Long operations need checkpoints, and the job must own or safely reference everything it touches. A window closing while a worker holds its raw pointer is not cancellation. It is delayed excitement.

I prefer a shared job queue such as ThreadWeaver for independent background work because it bounds concurrency and centralises scheduling. The caveat is that it does not make unsafe code safe. Shared caches still need locking, GUI objects still belong to the GUI thread, and dependencies between jobs still need explicit representation.

Priority needs restraint as well. Marking every visible thumbnail urgent merely recreates a first-in queue with more bookkeeping. I reserve priority for work whose delay is observable now, and still ensure ordinary jobs eventually run.

The current prototype sends immutable input into a job and returns a completed image through a queued signal. That copy may cost something, but the ownership story fits in one sentence. For now, clarity beats shaving a copy whose cost I have not measured.

Lock Before Looking

I reviewed a kernel path today that checked a pointer, acquired a spinlock, then used the pointer. It looked economical. It was also wrong.

if (device->queue) {
    spin_lock(&device->lock);
    flush_queue(device->queue);
    spin_unlock(&device->lock);
}

Another CPU can clear and free queue between the check and the lock. The check does not reserve anything; it merely records a fact that may expire immediately.

The simple repair is to protect both observation and use with the same lock:

spin_lock(&device->lock);
if (device->queue)
    flush_queue(device->queue);
spin_unlock(&device->lock);

This is safe only if flush_queue cannot sleep and does not acquire locks in an incompatible order. Spinlocks disable preemption in the relevant context; they are not permission to perform leisurely work. If flushing can block, I need a different design, perhaps detach the queue under the spinlock and process it later with appropriate lifetime ownership.

I prefer establishing ownership under one clearly documented lock rather than scattering “probably still valid” checks. The caveat is contention: a correct giant critical section can turn several processors into an expensive single processor.

The lesson is not “add locks.” It is “name the invariant, then guard every transition that can violate it.” Locks without an invariant are just punctuation with cache-line traffic.