Some UI components - a chart backed by a third-party rendering library, a media panel on a brittle codec stack, a plugin that occasionally takes the whole application down - need to be isolated from the main process without feeling bolted on. The technical problem is not just "how do we isolate the risky code?" It is "how do we isolate it without making the UI feel like a foreign island in the window?"
That is the gap this pattern tries to close. The child process renders the panel. The parent process still owns the window the user sees. The panel behaves like part of the same Qt Quick scene even though its pixels came from somewhere else.
What the obvious approaches miss
The first obvious approach is native window embedding: launch the panel in another process and stick that child window inside the parent window.
It sounds clean, but it gives the operating system control over most of the details you actually care about. The child window has its own focus behavior, repaint timing, scaling rules, stacking order, and capture semantics. If a tooltip from the parent crosses over that region, or the window moves between displays with different scale factors, or the child crashes mid-frame, you are now negotiating with window-system behavior instead of your own UI code.
That is tolerable when you truly want another window. It is a poor fit when what you want is "this rectangle inside my scene graph should be isolated, but it should still act like my rectangle."
The other common answer is a web view. That buys a mature out-of- process renderer, but it changes the job. Now you are solving HTML and browser integration problems: styling parity, composition differences, input quirks, accessibility boundaries, and a rendering model that was never your native UI in the first place.
Our problem was narrower. We did not want "remote content." We wanted "our own panel, rendered elsewhere, but still behaving like a native part of the window."
The shape of the boundary
The design is simple in outline:
- The child renders a Qt Quick scene offscreen.
- The parent imports the resulting image as a texture and composites
it inside an ordinary
QQuickItem. - The parent forwards input and containing-window state through a small message set.
- The child publishes completed frames with enough metadata for the parent to present them correctly.
The relevant Qt primitives are
QQuickItem for the parent-side scene item
and QQuickRenderControl for
offscreen Qt Quick rendering.
This is the design we use in Lumis: a dedicated child process drives an offscreen Qt Quick scene, and the frame channel from child to parent is a Sintra shared-memory ring.
Here is the same shape as a boundary sketch:
desktop OS
|
v
+---------------------- parent process -----------------------+
| real top-level window |
| Qt Quick scene graph |
| ordinary QQuickItem for the panel rectangle |
| |
| pointer/key/window state -------------------------------+ |
| send | |
| imported texture <- latest published frame metadata -----+ |
| final clipping / transforms / z-order / composition | |
+----------------------------------------------------------|--+
|
v
+---------------------- child process ----------------------+
| offscreen Qt Quick panel |
| consume input + state messages |
| render into shared buffer / publishable image slot |
| report frame number, size, replacement semantics, cursor |
+-----------------------------------------------------------+
There is no on-screen child window. The child never asks the OS for a piece of the desktop. From the operating system's point of view, the parent is just a normal Qt Quick application drawing one more textured item.
That detail is what makes the result believable. The parent still owns clipping, opacity, transforms, z-ordering, and the final repaint schedule. If a local popup overlaps the remote panel, the popup wins for the same reason it would win over any local item: the parent is compositing the whole scene.
The contract
The messages from parent to child fall into two categories: input and state.
Input is the straightforward part. If the user moves the mouse over
the panel, the parent receives a Qt pointer event first. It translates
the position from the item's local coordinates into the child panel's
logical coordinates and forwards a message such as "pointer moved to
(184, 96) with the left button still held." If the user spins the
wheel, presses a key, enters text, or leaves the panel entirely, the
parent forwards that too.
That sounds mundane, but it is an important ownership choice. The parent is not hoping the OS routes input to an embedded foreign window the right way. The parent decides what happened in its own UI and passes that fact across deliberately.
State messages carry the surrounding conditions the child needs to
stay in sync. Resize is the obvious example, but it is not the only
one. If the panel moves from a laptop display at 1.0x scale to an
external monitor at 2.0x, the parent sends the new scale factor. If
the user tabs into the panel, the parent sends focus state. If the
panel is hidden behind a collapsed sidebar, the parent can say so.
That split keeps responsibilities clean:
- The parent owns everything about the containing window.
- The child owns how its scene reacts once it knows the current input and state.
In practice, the boundary usually stays understandable if you can summarize it in two directions:
- Parent to child: pointer, wheel, key, text, and enter/leave events, because the parent is the first place those events become meaningful in the real window.
- Parent to child: size, scale factor, focus, and visibility, because only the parent knows the state of the containing window.
- Child to parent: buffer slot, image size, frame number, and replacement semantics, because the parent needs enough metadata to present the right frame.
- Child to parent: cursor intent, because the parent still sets the real cursor on the real window.
In the other direction, the child sends frames upward. The critical point is that it does not just dump pixels over the wall and hope for the best. Each published frame carries enough context for the parent to make a correct presentation decision.
For example, a frame update might include:
- Which shared buffer slot now contains the latest image.
- The logical size of that image.
- A monotonically increasing frame number.
- Whether this frame should replace the previous contents entirely.
That lets the parent answer practical questions without guesswork. Is this newer than what I already showed? Did the size change? Should I reuse existing texture state or rebuild it? Do I need to treat this as a clean replacement rather than an incremental update?
The child also reports cursor intent. If the child wants an I-beam over a text field or a resize cursor over a splitter, the parent sets that cursor locally. To the user, the panel still feels like part of the same application window instead of a foreign surface with slightly off behavior.
Where the work actually lives
At first glance, it is tempting to assume the hard part lives in the child, because the child is the remote process. In practice, most of the interesting work sits in the parent.
That division is easier to reason about when written down explicitly:
- The parent owns window integration: clipping, transforms, opacity, stacking, and the repaint schedule.
- The parent owns the meaning of input: it observes Qt events and translates coordinates before forwarding them.
- The parent owns display context: monitor changes, device-pixel ratio, focus state, and reconnect policy.
- The child owns rendering: consume the translated state, redraw, and publish completed frames.
- The child owns only its local health. If it becomes unhealthy, it can stop rendering or exit; the parent decides what the user sees next.
The child runtime can stay relatively narrow:
- Load the Qt Quick scene.
- Render it offscreen.
- Consume queued input and state updates on the GUI thread.
- Publish completed frames.
That is substantial work, but conceptually it is one responsibility: "act like a panel renderer."
The parent has the broader coordination job. It has to behave like the host that makes the remote panel feel native.
Concretely, that means things like:
- Mapping each incoming Qt event into the child panel's coordinate system.
- Re-sending baseline state after a reconnect, because a freshly restarted child knows nothing about the old session.
- Tracking device-pixel-ratio changes when the window moves between displays.
- Deciding what the user sees if the child exits, hangs, or crashes.
Take a simple drag interaction. The user presses on a slider inside the remote panel, drags fifty pixels to the right, and releases. The parent observes the press, move sequence, and release in its own item. It forwards those events in order. The child updates its scene, renders new frames, and publishes them back. The parent composites the newest available frame on each repaint. Neither side needs to pretend there is a native child window in the middle.
As a sequence sketch:
user parent item / host child renderer
---- ------------------ --------------
press -> receive Qt pointer press
map item coordinates
send pointer-press -----------> update scene state
render frame #41
import frame #41 <----------- publish frame #41
drag -> receive move events
coalesce or forward policy
send pointer-move(s) --------> update slider position
render frame #58, #59, ...
import newest frame <-------- publish newest frame
release -> receive Qt pointer release
send pointer-release --------> finalize interaction
render settled frame
composite latest frame <----- publish settled frame
That is the recurring theme: the boundary is explicit, but each side keeps a normal, understandable job.
Tradeoffs we accepted
This is still pixel transport. The child renders a frame, makes that frame available to the parent, and the parent composites it. That adds cost compared with drawing directly in one process, especially if the child has to read back from GPU memory before publishing.
For dashboard panels, inspectors, previews, and similar UI regions, that trade can be acceptable. For something closer to a game or a full screen video renderer, it probably is not.
The child is also not cheap in startup or memory terms. It is a real Qt Quick runtime with its own scene, event loop, and rendering path. That is a reasonable price for isolating a few risky panels. It is not an argument for spawning dozens of them casually.
Input handling needs policy. Suppose the child stops draining events for 300 milliseconds while the user is moving the mouse quickly. Does the parent queue every move? Coalesce them? Drop old motion events once the queue grows too large? Disconnect the session after a deadline? The pattern does not make that decision for you, but it does make the decision visible and local instead of burying it in window- system behavior.
Limits
This is process isolation for robustness, not a security sandbox. The child can crash without taking the parent down, which is the main benefit. But it is still trusted application code running with whatever authority you gave it. A malicious child is a different problem.
There is also a real first-frame cost. A local QQuickItem can start
painting as soon as its scene is ready. A remote panel has to launch,
initialize, render, and publish before the parent has meaningful
pixels to show. For always-visible panels that startup cost happens
once. For on-demand panels, you need a proper loading state.
The approach works best when parent and child evolve together. The message surface is intentionally small and closed. If you want the two sides to ship independently, protocol versioning becomes a primary design concern instead of an implementation detail.
And some forms of input are more demanding than simple pointer and key events. Rich IME behavior, complex accessibility interactions, and other high-context platform features need deliberate treatment. They do not fall out automatically from "forward basic events."
The useful mental model is not "remote window embedding." It is "remote rendering for one scene item." The parent owns the user-facing window and the interaction model. The child owns drawing a panel and reporting finished frames. Input goes down, pixels come up, and the boundary stays narrow enough that the panel still feels like it belongs in the same application.