Show the develop frame itself, instead of a photocopy of it
The oldest open item in the project (ARCH §6.1, spike S1, AC-8). Every frame
in develop was read off the GPU into a `SharedPixelBuffer` and handed back to
Slint to upload again: ~7 ms at 4K against a 0.28 ms compute pass, 96% of the
frame spent carrying pixels to the CPU and back so they could be drawn where
they already were.
Slint 1.17 will adopt a `wgpu::Texture` directly, and the whole of what that
needs is arrangement rather than code.
**One device, made before the window.** A texture belongs to the device that
allocated it, so the compute passes and the compositor cannot each open their
own. `GpuContext::new_shared` opens one and hands back the instance and
adapter alongside it; `dr_ui::shared_gpu` gives all four to
`BackendSelector::require_wgpu_29(WGPUConfiguration::Manual { .. })`. That
call has to come before the first window, because creating one selects a
backend for you — which is why the GPU is now opened at the top of `run`
rather than two hundred lines down beside the other controllers.
dr-gpu still names no UI type. It hands out raw wgpu and does not ask who is
compositing (ARCH §6.5a).
**Vulkan only on the shared path**, where headless keeps its GL fallback.
wgpu's GL backend reaches its display through EGL at instance creation, and
before a window exists there is no display handle to give it — so a GL
instance cannot later produce the window surface Slint needs from it. A
machine with no Vulkan gets no shared device and browses without develop,
which is the same degradation as no adapter at all.
**`renderer-femtovg` becomes `renderer-femtovg-wgpu`.** The old one is FemtoVG
over OpenGL and cannot be handed a wgpu texture at all. It is not kept
alongside as a fallback: FemtoVG-over-GL has no branch for an imported
texture, falls through to "render this image into a buffer", gets nothing, and
draws nothing — a blank canvas with no error, which is worse than the failure
it would be papering over. The consequence is stated plainly in the manifest:
the desktop app now needs a working wgpu adapter to open a window.
**Two output textures, not one, and this is the part that is not obvious.**
Slint repaints when the image property *changes*, and it decides that with
`PartialEq` — which for two images over the same `wgpu::Texture` says
"unchanged". A pass that reused a single target would have rendered every
slider move correctly on the GPU and shown none of them: right, and invisible.
`AdjustPass` alternates between two targets, so consecutive frames are
genuinely different values. It also settles the read-while-write question that
one queue was already answering.
`RENDER_ATTACHMENT` is added to both render targets. Neither pass uses it;
Slint rejects an imported texture without it, on the reasoning that a
compositor handed a texture may need to draw into it.
**`AdjustPass::read_output` is deleted rather than gated.** It and
`export_pixels` were the same transfer under two names, and the comments
explaining why they were separate are the point of the whole criterion:
reading pixels back to *display* them is the defect, reading them back to
*encode a file* is the only way a file is made. The display twin is now gone
outright, which is stronger than a feature flag — it cannot be turned back on.
`export_pixels` is untouched and still ungated. The `readback` feature comes
off dr-ui, darkroom-desktop and darkroom-android; it stays in dr-gpu, where it
still gates `RenderTarget::read_pixels` and the segmentation field readback.
`examples/develop` moves to `export_pixels`, which is honest — it writes a
PPM — and so no longer needs the feature.
Four tests, each named for what it protects and each of which fails without a
screen if the property it guards breaks:
- the adjust target satisfies every condition Slint's import checks, asserted
in the crate that owns the descriptor, because a descriptor that drifts
fails at runtime on a real display and nothing else would notice;
- consecutive renders are different textures, and the third is the first
again, so the alternation is a rotation and not an allocation per frame;
- the develop canvas has no CPU pixel buffer and does have a wgpu texture —
AC-8 itself, in the terms Slint uses;
- consecutive frames compare unequal as `slint::Image`, which is the property
the repaint actually depends on.
The zoom test's readback moves into the test module. It has to: there is no
library function that copies a displayed frame to the CPU any more, and that
is the point — the round-trip now exists in the test binary and nowhere a
shipping build can reach.
**What is not proven.** No GUI was run. What is verified is that the texture
satisfies the import contract, that the import succeeds, that the canvas is a
texture rather than a buffer, and that consecutive frames are distinguishable.
What is unverified is everything that needs a display: that Slint's FemtoVG
wgpu renderer adopts the Manual configuration on a real surface, that the
picture appears the right way up and the right colour, and the frame timing
that motivated the whole exercise. Android is untouched by testing — the
android backend routes a WGPU29 request to Skia, whose wgpu surface does
handle imported textures, but that is read from the source, not observed.
56 dr-gpu tests and 255 dr-ui tests pass, clippy clean under `-D warnings`,
fmt clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+89
-11
@@ -6,6 +6,12 @@
|
||||
//!
|
||||
//! Deliberately free of UI dependencies (ARCH §6.5a). The texture is handed
|
||||
//! out as a `wgpu::Texture`; who composites it is not this crate's concern.
|
||||
//!
|
||||
//! That independence is why [`GpuContext::new_shared`] hands back the raw
|
||||
//! instance and adapter rather than talking to a compositor itself: the
|
||||
//! compositor will only sample a texture that came from the device *it* draws
|
||||
//! with, so somebody has to make one device for both — but it does not have to
|
||||
//! be this crate, and this crate must not know who it is.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -33,19 +39,72 @@ pub struct GpuContext {
|
||||
adapter_info: wgpu::AdapterInfo,
|
||||
}
|
||||
|
||||
/// TRACES: FR-DSP-1 | AC-8
|
||||
/// One device, opened so that a compositor can be made to share it.
|
||||
///
|
||||
/// The texture the adjust pass writes only reaches the screen without a copy
|
||||
/// if the compositor is drawing with the *same* `wgpu::Device` — two devices
|
||||
/// are two address spaces, and a texture from one is not a texture the other
|
||||
/// can sample. So the device cannot be an implementation detail of either
|
||||
/// side; it has to be made once and handed to both.
|
||||
///
|
||||
/// [`Self::ctx`] is what the compute passes want. The instance and adapter are
|
||||
/// what a compositor wants in order to adopt the same setup — Slint's
|
||||
/// `WGPUConfiguration::Manual` asks for all four pieces — and they are handed
|
||||
/// out raw rather than wrapped, because naming Slint here would put a UI
|
||||
/// dependency in the one crate that must not have one (ARCH §6.5a).
|
||||
pub struct SharedGpu {
|
||||
/// The context every compute pass in this crate runs on.
|
||||
pub ctx: GpuContext,
|
||||
/// The instance the compositor will create its window surface from.
|
||||
pub instance: wgpu::Instance,
|
||||
/// The adapter [`Self::ctx`]'s device came from.
|
||||
pub adapter: wgpu::Adapter,
|
||||
}
|
||||
|
||||
impl GpuContext {
|
||||
/// Create a headless context — no surface, no window.
|
||||
///
|
||||
/// Used by tests and by the Slint path, which supplies its own surface.
|
||||
/// Used by tests, by the examples, and by anything that only needs to
|
||||
/// compute. A context opened this way cannot be shared with a compositor:
|
||||
/// see [`Self::new_shared`] for that, and for why the difference matters.
|
||||
pub async fn new_headless() -> Result<Self, GpuError> {
|
||||
// GL is allowed alongside Vulkan here and nowhere else: a machine with
|
||||
// no Vulkan loader should still run the tests, and a headless context
|
||||
// never has to produce a window surface — which is precisely the thing
|
||||
// the GL backend cannot do from an instance opened without a display
|
||||
// handle.
|
||||
Self::open(wgpu::Backends::VULKAN | wgpu::Backends::GL)
|
||||
.await
|
||||
.map(|shared| shared.ctx)
|
||||
}
|
||||
|
||||
/// TRACES: FR-DSP-1 | AC-8
|
||||
/// Open a device intended to be shared with the compositor.
|
||||
///
|
||||
/// Vulkan only, unlike [`Self::new_headless`]. The caller will hand the
|
||||
/// instance to a compositor that has to create a *window surface* from it,
|
||||
/// and wgpu's GL backend reaches its display through EGL at instance
|
||||
/// creation — an instance opened without a display handle, which is the
|
||||
/// only kind available before a window exists, cannot then produce a GL
|
||||
/// surface. Vulkan takes the window handle at surface creation instead, so
|
||||
/// it is the only backend this order of operations permits.
|
||||
///
|
||||
/// A machine with no Vulkan therefore gets no shared device, and the
|
||||
/// caller is expected to carry on without the develop path rather than
|
||||
/// refuse to start.
|
||||
pub async fn new_shared() -> Result<SharedGpu, GpuError> {
|
||||
// Vulkan on both targets (D1), and here it is not merely the
|
||||
// preference — see above.
|
||||
Self::open(wgpu::Backends::VULKAN).await
|
||||
}
|
||||
|
||||
async fn open(backends: wgpu::Backends) -> Result<SharedGpu, GpuError> {
|
||||
// `new_without_display_handle` rather than a struct literal: the
|
||||
// descriptor carries a boxed display handle and so has no `Default`,
|
||||
// and a headless context is precisely the case with no display to
|
||||
// hand it.
|
||||
// and there is no window yet to take one from in either case.
|
||||
let mut descriptor = wgpu::InstanceDescriptor::new_without_display_handle();
|
||||
// Vulkan on both targets (D1). GL is allowed as a fallback so a
|
||||
// machine without a Vulkan loader still runs the tests.
|
||||
descriptor.backends = wgpu::Backends::VULKAN | wgpu::Backends::GL;
|
||||
descriptor.backends = backends;
|
||||
let instance = wgpu::Instance::new(descriptor);
|
||||
|
||||
let adapter = instance
|
||||
@@ -76,7 +135,14 @@ impl GpuContext {
|
||||
// in compute shaders are required, and the downlevel tier
|
||||
// does not guarantee them. This is effectively our GPU
|
||||
// floor (NFR-COMPAT-1).
|
||||
required_limits: wgpu::Limits::default(),
|
||||
//
|
||||
// `using_resolution` raises only the texture-dimension limits,
|
||||
// to whatever this adapter actually offers. That matters once
|
||||
// a compositor shares this device: the default ceiling is
|
||||
// 8192, and a swapchain image for a large or scaled display
|
||||
// can exceed it — a limit we chose for our own compute passes
|
||||
// would otherwise silently cap somebody else's window.
|
||||
required_limits: wgpu::Limits::default().using_resolution(adapter.limits()),
|
||||
memory_hints: wgpu::MemoryHints::Performance,
|
||||
// Nothing behind a feature flag wgpu itself calls unstable —
|
||||
// the pipeline is ordinary compute and storage textures.
|
||||
@@ -88,10 +154,14 @@ impl GpuContext {
|
||||
.await
|
||||
.map_err(|e| GpuError::DeviceRequest(e.to_string()))?;
|
||||
|
||||
Ok(Self {
|
||||
device: Arc::new(device),
|
||||
queue: Arc::new(queue),
|
||||
adapter_info,
|
||||
Ok(SharedGpu {
|
||||
ctx: Self {
|
||||
device: Arc::new(device),
|
||||
queue: Arc::new(queue),
|
||||
adapter_info,
|
||||
},
|
||||
instance,
|
||||
adapter,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -259,8 +329,16 @@ impl RenderTarget {
|
||||
// STORAGE_BINDING to write from compute; TEXTURE_BINDING so the
|
||||
// compositor can sample it. COPY_SRC exists only for tests —
|
||||
// production never reads this back (ARCH §6.1).
|
||||
//
|
||||
// RENDER_ATTACHMENT is not something this pass ever uses. It is
|
||||
// there because Slint refuses to import a texture without it
|
||||
// (`TextureImportError::InvalidUsage`), the compositor having to
|
||||
// assume it may need to draw into what it was given. Declaring an
|
||||
// unused capability costs an allocation flag and buys the whole
|
||||
// zero-copy path, so it is a cheap price for AC-8.
|
||||
usage: wgpu::TextureUsages::STORAGE_BINDING
|
||||
| wgpu::TextureUsages::TEXTURE_BINDING
|
||||
| wgpu::TextureUsages::RENDER_ATTACHMENT
|
||||
| wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user