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