Move to wgpu 29, the version Slint can share a device with
Build and test / Desktop (Linux) (push) Failing after 38s
Build and test / Layer separation (push) Successful in 24s
Traceability / Requirement traces (push) Successful in 1m3s
🐳 Android image / Build and push (push) Successful in 3s
Build and test / android-image (push) Successful in 3s
Build and test / Android (aarch64) (push) Failing after 9m54s

Groundwork for spike S1. Importing a texture into a Slint scene requires it
to come from the *same* `wgpu::Device` Slint renders with, and Slint hands
out a device of the version it was compiled against. Slint 1.17 offers
`unstable-wgpu-28` and `unstable-wgpu-29` and nothing older, so wgpu 23 could
never have met it: two semver-incompatible wgpu crates in one tree are two
distinct types, and the device would not typecheck across the gap.

The version is therefore not a free choice, and the manifest now says so —
Slint and wgpu move together or not at all. The Slint requirement is also
corrected from "1.9" to the 1.17 it has actually been resolving to.

Nothing about the render path changes here. The readback bridge is still in
place and still the display path, so this is verified by the tests that
already existed rather than by anything new: 39 dr-gpu tests, which compare
real pixels off a real device, and 888 across the workspace, all passing.
Zero-copy lands separately and small.

What the six releases cost, in full:

  - `ImageCopyTexture`/`ImageCopyBuffer`/`ImageDataLayout` became the
    `TexelCopy*` names (24).
  - `Instance::new` takes the descriptor by value, and `InstanceDescriptor`
    lost its `Default` — it carries a boxed display handle now, so a headless
    context says `new_without_display_handle` and means it.
  - `request_adapter` returns `Result` rather than `Option` (24).
  - `DeviceDescriptor` absorbed the API trace from `request_device`'s second
    argument and gained `experimental_features` (25).
  - `PipelineLayoutDescriptor` takes `Option<&BindGroupLayout>` per slot, and
    `push_constant_ranges` became `immediate_size`.
  - `Maintain` became `PollType`, and `poll` is fallible.

Two of those are improvements worth having rather than churn. The error scope
is a guard whose `pop` runs on drop, so an early return from the pipeline
compiler no longer leaves a scope open on the device for whatever ran next to
fall into. And a fallible `poll` reports a lost device (NFR-R7) at the point
it happens, where before the map callback simply never arrived and the
failure surfaced later as a readback that spun out its poll limit.

Still to do for S1: dr-ui renders through `renderer-femtovg`, which is
OpenGL. Texture import needs Slint itself rendering on wgpu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 21:34:04 +02:00
co-authored by Claude Opus 5
parent b08e94405c
commit d7aeafaf84
8 changed files with 260 additions and 273 deletions
+26 -12
View File
@@ -105,8 +105,8 @@ impl AdjustPass {
.device
.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("adjust-layout"),
bind_group_layouts: &[&bind_group_layout],
push_constant_ranges: &[],
bind_group_layouts: &[Some(&bind_group_layout)],
immediate_size: 0,
});
Self {
@@ -127,7 +127,13 @@ impl AdjustPass {
// A validation error here is a codegen bug, not a user error.
// Push an error scope so it surfaces as a Result rather than a
// panic from wgpu's default handler.
self.ctx
//
// Since wgpu 29 the scope is a guard rather than a device-level
// push/pop pair, which is the better shape: an early return from
// this function pops it on drop instead of leaving a scope open on
// the device for whatever ran next to fall into.
let scope = self
.ctx
.device
.push_error_scope(wgpu::ErrorFilter::Validation);
@@ -151,7 +157,7 @@ impl AdjustPass {
cache: None,
});
if let Some(err) = pollster::block_on(self.ctx.device.pop_error_scope()) {
if let Some(err) = pollster::block_on(scope.pop()) {
return Err(GpuError::ShaderCompilation(format!(
"{err}\n\n--- generated source ---\n{}",
numbered(&shader.source)
@@ -335,15 +341,15 @@ impl AdjustPass {
let mut enc = self.ctx.device.create_command_encoder(&Default::default());
enc.copy_texture_to_buffer(
wgpu::ImageCopyTexture {
wgpu::TexelCopyTextureInfo {
texture: &target.texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::ImageCopyBuffer {
wgpu::TexelCopyBufferInfo {
buffer: &buf,
layout: wgpu::ImageDataLayout {
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(padded),
rows_per_image: Some(h),
@@ -376,7 +382,13 @@ impl AdjustPass {
// reporting an error.
let mut mapped = None;
for _ in 0..READBACK_POLL_LIMIT {
self.ctx.device.poll(wgpu::Maintain::Poll);
// A poll error is a lost device, which is exactly the case the
// bounded spin exists to escape — returning here reports it
// immediately rather than spinning out the full limit first.
self.ctx
.device
.poll(wgpu::PollType::Poll)
.map_err(|e| GpuError::Readback(e.to_string()))?;
match rx.try_recv() {
Ok(r) => {
mapped = Some(r);
@@ -485,15 +497,15 @@ mod tests {
let mut enc = ctx.device.create_command_encoder(&Default::default());
enc.copy_texture_to_buffer(
wgpu::ImageCopyTexture {
wgpu::TexelCopyTextureInfo {
texture: tex,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::ImageCopyBuffer {
wgpu::TexelCopyBufferInfo {
buffer: &buf,
layout: wgpu::ImageDataLayout {
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(padded),
rows_per_image: Some(h),
@@ -512,7 +524,9 @@ mod tests {
slice.map_async(wgpu::MapMode::Read, move |r| {
let _ = tx.send(r);
});
ctx.device.poll(wgpu::Maintain::Wait);
ctx.device
.poll(wgpu::PollType::wait_indefinitely())
.expect("poll");
rx.recv().expect("map").expect("map ok");
let data = slice.get_mapped_range();