Compare commits

..
Author SHA1 Message Date
irisandClaude Opus 5 0a5eb2d984 Build on wgpu 30
Two majors, and the renderer is under everything else that is left to
extract, so it goes before the slices that would otherwise be written twice.
`image` 0.25.6 -> 0.25.10 rides along; `winit` stays on 0.30.12, since 0.31
is only a prerelease.

What the API asked for:

- An instance now takes the display it will present on, and GLES on Wayland
  needs it -- so the window the surface is made from is handed over with it.
- `get_current_texture` returns a `CurrentSurfaceTexture` rather than a
  `Result`, which replaces an `unwrap` that would have panicked on a resize
  or an occluded window: reconfigure when the surface is outdated, lost or
  suboptimal, and skip the frame when there is nothing to draw into.
- Presenting moved to the queue, after `pre_present_notify` as before.
- Bind group layouts and vertex buffer layouts are sparse now, so a pipeline
  states `Some(layout)` per slot.

Verified with the three render checks and `tests/draw_cost.rs`: identical
pictures, and 33.6/167/587/2855 us per frame at 8/64/256/1024 layers against
33.3/161/588/2903 on wgpu 28.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 18:57:52 -04:00
8 changed files with 37 additions and 27 deletions

No files matched your search

+5 -1
View File
@@ -1,6 +1,10 @@
use crate::prelude::*;
use arboard::Clipboard;
use std::{marker::PhantomData, sync::Arc, time::Instant};
use std::{
marker::{PhantomData, Sized},
sync::Arc,
time::Instant,
};
use winit::{
event::{Ime, WindowEvent},
event_loop::{ActiveEventLoop, EventLoopProxy},
+5
View File
@@ -24,6 +24,8 @@ impl UiRenderer {
pub fn draw(&mut self) {
let output = match self.surface.get_current_texture() {
CurrentSurfaceTexture::Success(texture) => texture,
// Still drawable; the surface has just changed under us, and
// reconfiguring is what the next frame wants rather than this one.
CurrentSurfaceTexture::Suboptimal(texture) => {
self.surface.configure(&self.device, &self.config);
texture
@@ -32,6 +34,7 @@ impl UiRenderer {
self.surface.configure(&self.device, &self.config);
return;
}
// Nothing to draw into this frame.
CurrentSurfaceTexture::Timeout
| CurrentSurfaceTexture::Occluded
| CurrentSurfaceTexture::Validation => return,
@@ -78,6 +81,8 @@ impl UiRenderer {
pub fn new(window: Arc<Window>) -> Self {
let size = window.inner_size();
// The display handle is what GLES needs to present on Wayland, and it
// has to be the one the surface is made from.
let instance = Instance::new(InstanceDescriptor {
backends: Backends::PRIMARY,
display: Some(Box::new(window.clone())),
+2 -2
View File
@@ -4,7 +4,7 @@ mod max_size;
mod offset;
mod pad;
mod scroll;
mod set_size;
mod sized;
mod span;
mod stack;
@@ -14,6 +14,6 @@ pub use max_size::*;
pub use offset::*;
pub use pad::*;
pub use scroll::*;
pub use set_size::*;
pub use sized::*;
pub use span::*;
pub use stack::*;
@@ -1,12 +1,12 @@
use crate::prelude::*;
pub struct SetSize {
pub struct Sized {
pub inner: StrongWidget,
pub x: Option<Len>,
pub y: Option<Len>,
}
impl SetSize {
impl Sized {
fn apply_to_outer(&self, ctx: &mut SizeCtx) {
if let Some(x) = self.x {
ctx.outer.x.select_len(x.apply_rest());
@@ -17,7 +17,7 @@ impl SetSize {
}
}
impl Widget for SetSize {
impl Widget for Sized {
fn draw(&mut self, painter: &mut Painter) {
painter.widget(&self.inner);
}
+1 -1
View File
@@ -1,5 +1,5 @@
use crate::prelude::*;
use std::marker::Unsize;
use std::marker::{Sized, Unsize};
pub struct WidgetPtr {
pub inner: Option<StrongWidget>,
+1 -1
View File
@@ -1,5 +1,5 @@
use crate::prelude::*;
use std::marker::PhantomData;
use std::marker::{PhantomData, Sized};
pub struct TextBuilder<State, O = TextOutput, H: WidgetOption<State> = ()> {
pub content: String,
+6 -6
View File
@@ -31,9 +31,9 @@ widget_trait! {
}
}
fn sized(self, size: impl Into<Size>) -> impl WidgetFn<Rsc, SetSize> {
fn sized(self, size: impl Into<Size>) -> impl WidgetFn<Rsc, Sized> {
let size = size.into();
move |state| SetSize {
move |state| Sized {
inner: self.add_strong(state),
x: Some(size.x),
y: Some(size.y),
@@ -58,18 +58,18 @@ widget_trait! {
}
}
fn width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, SetSize> {
fn width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, Sized> {
let len = len.into();
move |state| SetSize {
move |state| Sized {
inner: self.add_strong(state),
x: Some(len),
y: None,
}
}
fn height(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, SetSize> {
fn height(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, Sized> {
let len = len.into();
move |state| SetSize {
move |state| Sized {
inner: self.add_strong(state),
x: None,
y: Some(len),
+14 -13
View File
@@ -4,19 +4,20 @@
//!
//! cargo test --release --test draw_cost -- --ignored --nocapture
//!
//! Wall time is the wrong number to read for anything under a few percent --
//! it varied by 2x between runs of one unchanged binary where instructions
//! retired varied by 0.1%. Count those instead:
//! **Read the instruction count, not the clock.** Wall time here swings by 2x
//! between runs of one binary on this machine -- more under `cargo test` than
//! run directly -- while instructions retired are stable to 0.1%:
//!
//! perf stat -e instructions:u target/release/.../draw_cost-* --ignored
//!
//! That is how `PrimitiveRender` was measured against a match in the renderer:
//! 6 instructions per list drawn, against the ~5,400 wgpu spends recording
//! one.
//! Measured that way on 2026-09-13, drawing each primitive through its own
//! `PrimitiveRender` rather than a match in the renderer costs **6
//! instructions per list drawn**, which is 0.1% of a frame at both 256 and
//! 1024 layers. Recording one list into the pass costs wgpu ~5,400.
//!
//! The instance is leaked deliberately. A Vulkan loader may unload the driver
//! when the last one drops, which can fault as a thread that used it exits --
//! and every test runs on a spawned thread.
//! The instance is leaked on purpose. Dropping the last one makes the Vulkan
//! loader unload Mesa's ICD, which faults when a thread that touched Vulkan
//! exits -- and libtest runs every test on a spawned thread.
use std::time::Instant;
@@ -29,13 +30,13 @@ use wgpu::{Color as GpuColor, *};
const SIZE: u32 = 1024;
const FRAMES: u32 = 200;
/// Reported as the best of this many batches, since the mean moves by more
/// than the thing being measured.
/// Reported as the best of this many batches. The mean moves by 15% between
/// runs on this machine, which is more than the thing being measured.
const BATCHES: u32 = 8;
fn gpu() -> Option<(Device, Queue)> {
// Probed rather than assumed: there may be no Vulkan adapter, and GL is
// what is left when there is not.
// Probed rather than assumed: this machine's Vulkan device comes and goes,
// and GL is what is left when it is gone.
let all = Instance::new(InstanceDescriptor::new_without_display_handle());
let instance = match pollster::block_on(all.request_adapter(&RequestAdapterOptions::default()))
{