mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-30 13:08:48 -06:00
* Windows: vendor egui-wgpu 0.36.2 as published (#577) The crate as released on crates.io, plus egui's LICENSE-APACHE and LICENSE-MIT from the 0.36.2 tag (the published crate leaves them out). Not used yet: the next commit patches it and adds it to [patch.crates-io]. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Windows: a window larger than the GPU's texture limit can't close PdfCraft (#577) egui-wgpu 0.36.2 configures a window's surface at the window's own size. Beyond the device's max_texture_dimension_2d, Surface::configure fails validation and wgpu's default error handler panics. eframe configures the surface before the app is created, so no app code can intercept it (emilk/egui#8361). #578 raised the device's limit to the adapter's; this removes the crash for windows larger than even that. vendor/egui-wgpu: `winit::surface_fit` shrinks both sides by one factor so neither exceeds the limit (the longer side lands exactly on it), and egui draws at that factor into the surface. On DX12, PdfCraft's Windows default, the swapchain is stretched over the window (DXGI_SCALING_STRETCH), so the window stays complete and lined up with the pointer, only softer; wgpu's GL backend copies it unscaled into a corner instead. It applies wherever a surface is sized: install, resize (start-up restore, maximise, DPI change, fullscreen) and a recreated surface, which starts again from the window's own size. Depth and MSAA textures follow. A surface logs a warning when it becomes fitted. Why only an Outlook launch hit it (read from the code, not observed): winit creates the window at CW_USEDEFAULT, so Windows places it, and a launcher that hands ShellExecute its own window gets new windows on its monitor. winit then converts the restored logical size at that monitor's scale: 3440 points became 8600 pixels on a 250% panel. eframe's clamp compares points against the largest monitor, so it let 3440 through. Tests: surface_fit with the issue's 8600 x 3423 and edge sizes (zero, u32::MAX, a zero limit, the longer side exactly on the limit); egui-wgpu resolves to vendor/egui-wgpu and the fit stays wired into the paths that size a surface. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * vendor/egui-wgpu: drop the stray .cargo-ok marker --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
Brandon Thomas
parent
1f0466fcae
commit
f7d5ca4a07
Generated
-2
@@ -1474,8 +1474,6 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "egui-wgpu"
|
||||
version = "0.36.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5cef1f6d1bba40b09a4d0d62cb822b81030922cfbf65c5373398c3a3caafc5bd"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"bytemuck",
|
||||
|
||||
@@ -99,3 +99,4 @@ hayro-syntax = { path = "vendor/hayro-syntax" }
|
||||
hayro-jbig2 = { path = "vendor/hayro-jbig2" }
|
||||
lopdf = { path = "vendor/lopdf" }
|
||||
winit = { path = "vendor/winit" }
|
||||
egui-wgpu = { path = "vendor/egui-wgpu" }
|
||||
|
||||
@@ -137,6 +137,7 @@ M0 → M1 → M2 → M3 → M4 must happen in order. After M4, M5–M12 can run
|
||||
- **2026-10-09 (M2, JPX sample normalization):** JPEG 2000 rescaling uses bounded integer arithmetic for supported 1–32-bit samples, including the 32-bit boundary that previously shifted past the integer width. Zero-sized rows are refused before scaling. Small contributor-original codestreams exercise decoding and rendering; endpoint and standard-depth regressions cover the same helper. Overall estimate unchanged (about 30–35%).
|
||||
|
||||
- **2026-10-09 (M14, #577):** The desktop GPU setup enables the selected adapter's supported 2D texture extent instead of egui-wgpu's fixed 8192-pixel extent. This removes the artificial surface ceiling behind a mixed-DPI Windows startup crash when opening Outlook PDF attachments. Synthetic regressions cover the observed 8600 × 3423 surface, downlevel adapters and preservation of other device limits. Hardware resolution limits still apply; milestone and overall effort estimates are unchanged.
|
||||
- **2026-10-09 (M14, #577 follow-up):** A window larger than the GPU's texture limit no longer closes PdfCraft. Vendored egui-wgpu 0.36.2 fits the window's surface within `max_texture_dimension_2d` and draws egui at the matching scale, on every path that sizes a surface (start-up restore, resize, maximise, DPI change, a recreated surface); upstream is emilk/egui#8361. Root cause of the Outlook-only trigger: winit creates the window on the launching app's monitor and converts the restored size at that monitor's scale. Unit tests cover the 8600 × 3423 case and edge sizes; a Windows Sandbox (WARP) run of a window one pixel over the limit panicked before and kept drawing after. Estimates unchanged.
|
||||
|
||||
- **2026-10-09 (M5, Fill & Sign flatten on save):** Preferences and `doc_save` can bake Fill & Sign text, marks and signatures into the page when saving. The option defaults off. Other comments and form fields stay, a second flatten does not reuse an XObject name already on the page, and a document that cannot be changed is left unwritten. Overall estimate unchanged (about 30–35%).
|
||||
- **2026-10-09 (M5.7, #434 partial):** Natural-size image stamps placed at a click or through `stamp_custom` remain upright on rotated pages, with a displayed-space-sized rectangle and a counterrotated standard appearance matrix. Move, resize, restyle and undo/redo preserve that orientation. Native and external Poppler save/reopen rendering cover all four quarter-turns; UI tests verify the displayed bounds. Explicit rectangle placement, image signatures, PDF-page stamps and image-file orientation metadata remain outside this repair. #434 stays open; milestone table and effort estimates remain unchanged.
|
||||
|
||||
@@ -626,6 +626,82 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_window_larger_than_the_gpu_limit_gets_a_surface_within_it() {
|
||||
use eframe::egui_wgpu::winit::surface_fit;
|
||||
// Issue #577: 3440 x 1369 points restored at 250% asked an 8192-pixel device for this.
|
||||
let (width, height, scale) = surface_fit(8600, 3423, 8192);
|
||||
assert_eq!((width, height), (8192, 3260));
|
||||
// Both sides shrink alike and egui draws at that factor: the whole window is drawn.
|
||||
assert!((8600.0 * scale - 8192.0).abs() < 0.01, "{scale}");
|
||||
assert!((3423.0 * scale - height as f32).abs() < 1.0, "{scale}");
|
||||
// Within the limit nothing changes, up to and including the limit itself.
|
||||
assert_eq!(surface_fit(8600, 3423, 16384), (8600, 3423, 1.0));
|
||||
assert_eq!(surface_fit(8192, 8192, 8192), (8192, 8192, 1.0));
|
||||
assert_eq!(surface_fit(0, 0, 8192), (0, 0, 1.0));
|
||||
// One side over the limit: the window as it would be stretched across monitors.
|
||||
assert_eq!(surface_fit(8193, 600, 8192).0, 8192);
|
||||
assert_eq!(surface_fit(600, 8193, 8192).1, 8192);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn surface_fit_never_empties_or_overflows_a_surface() {
|
||||
use eframe::egui_wgpu::winit::surface_fit;
|
||||
for (w, h, max) in [
|
||||
(1, u32::MAX, 8192),
|
||||
(u32::MAX, 1, 2048),
|
||||
(u32::MAX, u32::MAX, 16384),
|
||||
(0, u32::MAX, 8192),
|
||||
(u32::MAX, 0, 8192),
|
||||
(5, 7, 0),
|
||||
(40_000, 3, 1),
|
||||
] {
|
||||
let (fw, fh, scale) = surface_fit(w, h, max);
|
||||
assert!(fw <= max.max(1) && fh <= max.max(1), "{w} x {h} in {max}: {fw} x {fh}");
|
||||
// A side that wasn't zero stays non-zero (`Surface::configure` rejects an empty one),
|
||||
// and a zero side stays zero (egui-wgpu skips configuring it).
|
||||
assert_eq!((fw == 0, fh == 0), (w == 0, h == 0), "{w} x {h} in {max}: {fw} x {fh}");
|
||||
assert!(scale.is_finite() && scale > 0.0 && scale <= 1.0, "{w} x {h} in {max}: {scale}");
|
||||
}
|
||||
// The longer side lands exactly on the limit (rounding never leaves it a pixel short),
|
||||
// and the shorter side keeps the window's proportions to within a pixel.
|
||||
for max in [2048, 8192, 16384] {
|
||||
for long in (max + 1..=max * 5).step_by(997).chain([max * 2, max * 4, u32::MAX]) {
|
||||
for short in [1, 3, 600, max / 3, max - 1, max, long - 1, long] {
|
||||
let (fw, fh, scale) = surface_fit(long, short, max);
|
||||
assert_eq!(fw, max, "{long} x {short} in {max}");
|
||||
let expected = f64::from(short) * f64::from(max) / f64::from(long);
|
||||
assert!((f64::from(fh) - expected).abs() <= 1.0, "{long} x {short} in {max}: {fh}");
|
||||
assert_eq!(surface_fit(short, long, max), (fh, fw, scale), "transposed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn egui_wgpu_carries_the_surface_size_fit() {
|
||||
// Issue #577: egui-wgpu 0.36.2 configures a window's surface at the window's size, and
|
||||
// `Surface::configure` panics when that's beyond the device's `max_texture_dimension_2d`
|
||||
// (emilk/egui#8361). vendor/egui-wgpu fits it within the limit. A dependency bump that
|
||||
// resolves egui-wgpu from crates.io again, or a re-vendored copy without the patch, would
|
||||
// bring the crash back: re-apply the patch, or drop the copy once an egui release has a
|
||||
// fix (vendor/README.md).
|
||||
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
let lock = std::fs::read_to_string(root.join("Cargo.lock")).unwrap();
|
||||
let egui_wgpu = lock.split("[[package]]").find(|p| p.contains("\nname = \"egui-wgpu\"\n")).expect("egui-wgpu is in Cargo.lock");
|
||||
assert!(!egui_wgpu.contains("\nsource = "), "egui-wgpu must resolve to vendor/egui-wgpu, not:{egui_wgpu}");
|
||||
// `surface_fit` is tested above; these keep it in the paths that size a surface.
|
||||
let painter = std::fs::read_to_string(root.join("vendor/egui-wgpu/src/winit.rs")).unwrap().replace("\r\n", "\n");
|
||||
for patch in [
|
||||
"let (width, height, render_scale) = surface_fit(window_width, window_height, max_side);",
|
||||
"let (fit_width, fit_height, _) = surface_fit(width, height, self.max_surface_side());",
|
||||
"pixels_per_point: pixels_per_point * surface_state.render_scale,",
|
||||
"old_state.window_width,\n old_state.window_height,",
|
||||
] {
|
||||
assert!(painter.contains(patch), "vendor/egui-wgpu lost its surface size patch: {patch}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn winit_carries_the_windows_11_monitor_scale_fix() {
|
||||
// Issue #324: winit 0.30.13 as released nudges a window dragged onto a monitor with another
|
||||
|
||||
+6
-2
@@ -29,8 +29,12 @@ covers every target starting with it (`pdfcraft*=debug`). The logger is
|
||||
The desktop renderer enables the selected adapter's supported 2D texture extent, while keeping
|
||||
egui-wgpu's other device requirements. Restored windows on mixed-DPI monitors can exceed its
|
||||
default 8192-pixel extent even when the GPU supports a larger surface (#577). The enabled extent
|
||||
never exceeds the adapter's capability; this does not make surfaces larger than the hardware
|
||||
limit renderable.
|
||||
never exceeds the adapter's capability. A window larger than even that (restored at another
|
||||
monitor's scale, or stretched across monitors) gets a surface fitted within the limit and is drawn
|
||||
at a correspondingly lower scale instead of panicking in `Surface::configure` (vendored egui-wgpu,
|
||||
`surface_fit`; see `vendor/README.md`). On DX12, PdfCraft's Windows default, the surface is
|
||||
stretched over the window: softer, but complete and lined up with the pointer. wgpu's GL backend
|
||||
copies it unscaled into a corner of the window instead.
|
||||
|
||||
## Environment variables
|
||||
|
||||
|
||||
Vendored
+1
@@ -14,6 +14,7 @@ the fix. Vendoring copyleft code is never allowed (plan/adr/0001).
|
||||
| lopdf | 0.45.0 | MIT | (1) PNG predictor rows longer than the data are refused before their buffers are allocated (a fuzzed `/Columns 4294967295` allocated two 4 GiB rows), and empty frames return without row buffers; (2) predictor row widths use saturating arithmetic or reject overflow; (3) sub-byte TIFF Predictor 2 scratch is bounded by the samples present in the input instead of the declared `/Colors`, aligned with [lopdf#589](https://github.com/J-F-Liu/lopdf/pull/589) | (1) `inspect::tests::png_predictor_rows_longer_than_the_data_are_refused_before_allocating`, `inspect::tests::empty_png_predictor_frames_do_not_allocate_rows`; (2) `inspect::tests::tiff_subbyte_predictor_row_width_overflow_is_refused`; (3) `inspect::tests::tiff_subbyte_predictor_scratch_is_bounded_by_available_samples`, `inspect::tests::tiff_subbyte_predictors_keep_components_rows_and_padding` |
|
||||
| hayro-syntax | 0.7.2 | Apache-2.0 OR MIT | (1) Page-tree cycle guard in `resolve_pages` (visited set + depth cap): a `/Kids` loop among compressed objects overflowed the stack and aborted the process; (2) `ccitt_size_ok`: CCITT images wider than 2^20 columns or over 2^28 pixels are not decoded (a fuzzed `/Columns 4294967295` allocated 4 GiB); (3) Flate/LZW predictor row widths are saturating `usize`, a zero width is refused and a row longer than the data is not allocated (a fuzzed `/Columns 9223372036854775807` wrapped to a 2^61-byte allocation and aborted); (4) the inline-image end-of-data check (`EI`) doesn't nest inside its own look-ahead and examines at most 256 candidates and steps (50 images with "EI" in their data ran for over ten minutes); (5) a stream whose `/Length` is wrong is recovered only when `stream` is the next token — scanning ahead to a later object's `stream` made an indirect appearance-state dictionary (Quartz checkboxes: `/AP /N` → `<< /Yes … /Off … >>`) render as a non-form, so the widget was skipped; (6) JPX rescaling uses an exact integer sample range for 1–32-bit components (32 shifted past u32, and floating-point maxima at 25–31 bits wrapped to zero), and refuses zero-sized rows; (7) `decode_limit`: Flate (through flate2 and the fallback inflater), LZW and RunLength never write past `MAX_DECODED_STREAM` (256 MiB), or, for the last filter of an image of known size, past twice its pixel bytes (a predictor tag byte per byte), at most 1 GiB; `Stream::decoded_within` for budgeted callers; a page's `/Contents` array shares one `MAX_DECODED_STREAM` (a 2 MB page whose content stream or 8 × 8 image inflated to 2 GB took the renderer past 3 GB in under a second; naming the stream twenty times in `/Contents`, 5 GB); (8) high-precision numbers (jsPDF MediaBox coordinates with over 19 digits) parse without overflowing the u64 mantissa: fractional digits past it are dropped, integer digits past it scale the value (a real, kept finite); (9) large dictionary indexes are built on first use and shared across repeated reads within each PDF (#307); the validated-byte cache is bounded, reader contexts remain separate, repairs and inline/content dictionaries bypass sharing, and small dictionaries stay eager; (10) decoded object-stream slots publish once under contention, cold loading rejects recursive dependencies, and xref repair serializes without holding recursive read guards; this permits one parser per native render pool with private worker caches; (11) `sync.rs`: a poisoned lock is recovered instead of unwrapped (a worker panic no longer cascades into every reader of the same PDF), and xref repair takes a blocking write lock instead of asserting a non-blocking one; (12) `SegmentList` (`util.rs`) returns `None` for an out-of-range index instead of panicking | (1) `cos/tests/writer.rs::page_tree_loops_in_object_streams_do_not_crash_readers`; (2) `raster::tests::absurd_mask_and_fax_sizes_are_refused_before_decoding`; (3) `raster::tests::absurd_predictor_columns_and_line_widths_render`; (4) `raster::tests::inline_images_with_ei_in_their_data_parse_in_linear_time`; (5) `raster::tests::indirect_appearance_state_dict_is_not_another_stream`; (6) `raster::tests::jpx_stream_bit_depth_boundaries_do_not_panic`, `raster::tests::jpx_32_bit_image_does_not_prevent_other_page_content_from_rendering`, `raster::tests::jpx_gray_rgb_and_alpha_keep_eight_bit_samples`, `raster::tests::jpx_streams_reject_out_of_range_bit_depths`; (7) `raster::tests::decompression_bombs_stop_at_the_stream_limit`, `raster::tests::page_contents_and_nested_forms_share_one_budget`; (8) `inspect::tests::jspdf_high_precision_mediabox_does_not_overflow_dimensions`, `object::number::tests::high_precision_float_does_not_overflow_mantissa` (hayro-syntax); (9) `object::dict::lazy_index_tests`, `raster::tests::large_resource_indexes_preserve_pixels_and_text`; (10) `data::tests`, `xref::tests::concurrent_cold_object_stream_reads_all_succeed`, `raster::tests::shared_parser_is_lazy_and_initializes_once_under_contention`; (11) `data::tests::panicking_initializer_wakes_waiters_and_prevents_retries`, `xref::tests::concurrent_cold_object_stream_reads_all_succeed`; (12) `util::tests::segment_list_rejects_out_of_range_indices` |
|
||||
| winit | 0.30.13 | Apache-2.0 | (1) on Windows 11 (build 22000 and later), `WM_DPICHANGED` applies the rect Windows suggests. It used to nudge the window onto the monitor `MonitorFromWindow` names, which during a drag is still the monitor being left; the nudge sent another `WM_DPICHANGED`, so a window dragged to a monitor with another scale factor grew and stayed on the wrong monitor at the wrong scale (#324). Backport of winit master 488c036a ([winit#4341](https://github.com/rust-windowing/winit/pull/4341)), which no 0.30 release has; Windows 10 keeps the old adjustment, as on winit master. The published crate's asset files (`docs/res/*.svg`, `examples/data/*.png`) are left out, so its `window` example doesn't build | (1) `pdfcraft` `tests::winit_carries_the_windows_11_monitor_scale_fix` keeps the patch in the build; the drag itself needs two monitors with different scale factors on Windows 11 |
|
||||
| egui-wgpu | 0.36.2 | MIT OR Apache-2.0 | (1) `winit::surface_fit`: a window's surface is configured within the device's `max_texture_dimension_2d`, both sides shrunk by one factor that egui then draws at (`pixels_per_point × render_scale`), so the whole window is drawn into the surface (on DX12 it is stretched over the window and lines up with the pointer; wgpu's GL backend copies it unscaled into a corner); the longer side becomes exactly the limit; the depth and MSAA textures follow, and a recreated surface starts again from the window's own size. egui-wgpu configured the window's size as is, and `Surface::configure` panics beyond the limit: a window restored at another monitor's scale (opening an Outlook attachment on a mixed-DPI laptop asked an 8192-pixel device for 8600 × 3423, #577) or stretched across monitors closed PdfCraft, before its window appeared when it happened at start-up ([emilk/egui#8361](https://github.com/emilk/egui/issues/8361)). The published crate leaves out egui's licence files; `LICENSE-APACHE` and `LICENSE-MIT` are from the 0.36.2 tag | (1) `pdfcraft` `tests::a_window_larger_than_the_gpu_limit_gets_a_surface_within_it`, `tests::surface_fit_never_empties_or_overflows_a_surface`, and `tests::egui_wgpu_carries_the_surface_size_fit`, which keeps the patch in the build and in the paths that size a surface |
|
||||
|
||||
Temporary: hayro is the bootstrap renderer and lopdf the bootstrap inspector (ADR-0004); M2.6 replaces them with our own devices and `cos`/`model`.
|
||||
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"git": {
|
||||
"sha1": "49682f8baa058bf49e011035cfbd6e825f88a5ef"
|
||||
},
|
||||
"path_in_vcs": "crates/egui-wgpu"
|
||||
}
|
||||
+2719
File diff suppressed because it is too large
Load Diff
Vendored
+340
@@ -0,0 +1,340 @@
|
||||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
edition = "2024"
|
||||
rust-version = "1.95"
|
||||
name = "egui-wgpu"
|
||||
version = "0.36.2"
|
||||
authors = [
|
||||
"Nils Hasenbanck <nils@hasenbanck.de>",
|
||||
"embotech <opensource@embotech.com>",
|
||||
"Emil Ernerfeldt <emil.ernerfeldt@gmail.com>",
|
||||
]
|
||||
build = false
|
||||
include = [
|
||||
"../../LICENSE-APACHE",
|
||||
"../../LICENSE-MIT",
|
||||
"**/*.rs",
|
||||
"**/*.wgsl",
|
||||
"Cargo.toml",
|
||||
]
|
||||
autolib = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autotests = false
|
||||
autobenches = false
|
||||
description = "Bindings for using egui natively using the wgpu library"
|
||||
homepage = "https://github.com/emilk/egui/tree/main/crates/egui-wgpu"
|
||||
readme = "README.md"
|
||||
keywords = [
|
||||
"wgpu",
|
||||
"egui",
|
||||
"gui",
|
||||
"gamedev",
|
||||
]
|
||||
categories = [
|
||||
"gui",
|
||||
"game-development",
|
||||
]
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/emilk/egui/tree/main/crates/egui-wgpu"
|
||||
resolver = "2"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
[features]
|
||||
capture = ["dep:egui"]
|
||||
default = [
|
||||
"fragile-send-sync-non-atomic-wasm",
|
||||
"macos-window-resize-jitter-fix",
|
||||
"wgpu/default",
|
||||
"wgpu/webgl",
|
||||
]
|
||||
fragile-send-sync-non-atomic-wasm = ["wgpu/fragile-send-sync-non-atomic-wasm"]
|
||||
macos-window-resize-jitter-fix = ["wgpu/metal"]
|
||||
wayland = ["winit?/wayland"]
|
||||
winit = [
|
||||
"dep:winit",
|
||||
"winit/rwh_06",
|
||||
"dep:egui",
|
||||
"capture",
|
||||
]
|
||||
x11 = ["winit?/x11"]
|
||||
|
||||
[lib]
|
||||
name = "egui_wgpu"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies.ahash]
|
||||
version = "0.8.12"
|
||||
features = [
|
||||
"no-rng",
|
||||
"std",
|
||||
]
|
||||
default-features = false
|
||||
|
||||
[dependencies.bytemuck]
|
||||
version = "1.25"
|
||||
|
||||
[dependencies.document-features]
|
||||
version = "0.2.12"
|
||||
|
||||
[dependencies.egui]
|
||||
version = "0.36.2"
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[dependencies.epaint]
|
||||
version = "0.36.2"
|
||||
features = ["bytemuck"]
|
||||
default-features = false
|
||||
|
||||
[dependencies.log]
|
||||
version = "0.4.33"
|
||||
features = ["std"]
|
||||
|
||||
[dependencies.profiling]
|
||||
version = "1.0"
|
||||
default-features = false
|
||||
|
||||
[dependencies.thiserror]
|
||||
version = "2.0"
|
||||
|
||||
[dependencies.type-map]
|
||||
version = "0.5.1"
|
||||
|
||||
[dependencies.web-time]
|
||||
version = "1.1"
|
||||
|
||||
[dependencies.wgpu]
|
||||
version = "30.0"
|
||||
features = [
|
||||
"std",
|
||||
"wgsl",
|
||||
]
|
||||
default-features = false
|
||||
|
||||
[dependencies.winit]
|
||||
version = "0.30.13"
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[lints.clippy]
|
||||
allow_attributes = "warn"
|
||||
as_ptr_cast_mut = "warn"
|
||||
assigning_clones = "allow"
|
||||
bool_to_int_with_if = "warn"
|
||||
branches_sharing_code = "warn"
|
||||
cast_possible_truncation = "allow"
|
||||
cast_possible_wrap = "allow"
|
||||
checked_conversions = "warn"
|
||||
clear_with_drain = "warn"
|
||||
clone_on_ref_ptr = "warn"
|
||||
cloned_instead_of_copied = "warn"
|
||||
coerce_container_to_any = "warn"
|
||||
comparison_chain = "allow"
|
||||
dbg_macro = "warn"
|
||||
debug_assert_with_mut_call = "warn"
|
||||
decimal_bitwise_operands = "warn"
|
||||
default_union_representation = "warn"
|
||||
derive_partial_eq_without_eq = "warn"
|
||||
disallowed_script_idents = "warn"
|
||||
doc_broken_link = "warn"
|
||||
doc_comment_double_space_linebreaks = "warn"
|
||||
doc_include_without_cfg = "warn"
|
||||
doc_link_with_quotes = "warn"
|
||||
doc_markdown = "warn"
|
||||
duration_suboptimal_units = "warn"
|
||||
elidable_lifetime_names = "warn"
|
||||
empty_enum_variants_with_brackets = "warn"
|
||||
empty_enums = "warn"
|
||||
enum_glob_use = "warn"
|
||||
equatable_if_let = "warn"
|
||||
exit = "warn"
|
||||
expl_impl_clone_on_copy = "warn"
|
||||
explicit_deref_methods = "warn"
|
||||
explicit_into_iter_loop = "warn"
|
||||
explicit_iter_loop = "warn"
|
||||
fallible_impl_from = "warn"
|
||||
filter_map_next = "warn"
|
||||
flat_map_option = "warn"
|
||||
float_cmp_const = "warn"
|
||||
fn_params_excessive_bools = "warn"
|
||||
fn_to_numeric_cast_any = "warn"
|
||||
format_push_string = "warn"
|
||||
from_iter_instead_of_collect = "warn"
|
||||
get_unwrap = "warn"
|
||||
ignore_without_reason = "warn"
|
||||
ignored_unit_patterns = "warn"
|
||||
implicit_clone = "warn"
|
||||
imprecise_flops = "warn"
|
||||
inconsistent_struct_constructor = "warn"
|
||||
index_refutable_slice = "warn"
|
||||
inefficient_to_string = "warn"
|
||||
infinite_loop = "warn"
|
||||
into_iter_without_iter = "warn"
|
||||
invalid_upcast_comparisons = "warn"
|
||||
ip_constant = "warn"
|
||||
iter_filter_is_ok = "warn"
|
||||
iter_filter_is_some = "warn"
|
||||
iter_not_returning_iterator = "warn"
|
||||
iter_on_empty_collections = "warn"
|
||||
iter_on_single_items = "warn"
|
||||
iter_over_hash_type = "warn"
|
||||
iter_without_into_iter = "warn"
|
||||
large_digit_groups = "warn"
|
||||
large_futures = "warn"
|
||||
large_include_file = "warn"
|
||||
large_stack_arrays = "warn"
|
||||
large_stack_frames = "warn"
|
||||
large_types_passed_by_value = "warn"
|
||||
let_underscore_must_use = "allow"
|
||||
let_underscore_untyped = "allow"
|
||||
linkedlist = "warn"
|
||||
literal_string_with_formatting_args = "warn"
|
||||
lossy_float_literal = "warn"
|
||||
macro_use_imports = "warn"
|
||||
manual_assert = "warn"
|
||||
manual_ilog2 = "warn"
|
||||
manual_instant_elapsed = "warn"
|
||||
manual_is_power_of_two = "warn"
|
||||
manual_is_variant_and = "warn"
|
||||
manual_let_else = "warn"
|
||||
manual_midpoint = "warn"
|
||||
manual_range_contains = "allow"
|
||||
manual_string_new = "warn"
|
||||
map_err_ignore = "warn"
|
||||
map_unwrap_or = "allow"
|
||||
match_bool = "warn"
|
||||
match_same_arms = "warn"
|
||||
match_wild_err_arm = "warn"
|
||||
match_wildcard_for_single_variants = "warn"
|
||||
mem_forget = "warn"
|
||||
mismatching_type_param_order = "warn"
|
||||
missing_assert_message = "warn"
|
||||
missing_errors_doc = "warn"
|
||||
missing_fields_in_debug = "warn"
|
||||
mut_mut = "warn"
|
||||
mutex_integer = "warn"
|
||||
needless_continue = "warn"
|
||||
needless_for_each = "warn"
|
||||
needless_pass_by_ref_mut = "warn"
|
||||
needless_pass_by_value = "warn"
|
||||
needless_raw_string_hashes = "warn"
|
||||
needless_type_cast = "warn"
|
||||
negative_feature_names = "warn"
|
||||
non_std_lazy_statics = "warn"
|
||||
non_zero_suggestions = "warn"
|
||||
nonstandard_macro_braces = "warn"
|
||||
option_as_ref_cloned = "warn"
|
||||
option_option = "warn"
|
||||
or_fun_call = "warn"
|
||||
path_buf_push_overwrite = "warn"
|
||||
pathbuf_init_then_push = "warn"
|
||||
precedence_bits = "warn"
|
||||
print_stderr = "warn"
|
||||
print_stdout = "warn"
|
||||
ptr_as_ptr = "warn"
|
||||
ptr_cast_constness = "warn"
|
||||
pub_underscore_fields = "warn"
|
||||
pub_without_shorthand = "warn"
|
||||
rc_mutex = "warn"
|
||||
redundant_type_annotations = "warn"
|
||||
ref_as_ptr = "warn"
|
||||
ref_option = "warn"
|
||||
ref_option_ref = "warn"
|
||||
ref_patterns = "warn"
|
||||
rest_pat_in_fully_bound_structs = "warn"
|
||||
return_and_then = "warn"
|
||||
same_functions_in_if_condition = "warn"
|
||||
same_length_and_capacity = "warn"
|
||||
self_named_module_files = "allow"
|
||||
self_only_used_in_recursion = "warn"
|
||||
semicolon_if_nothing_returned = "warn"
|
||||
set_contains_or_insert = "warn"
|
||||
should_panic_without_expect = "allow"
|
||||
significant_drop_tightening = "allow"
|
||||
single_char_pattern = "warn"
|
||||
single_match_else = "warn"
|
||||
single_option_map = "warn"
|
||||
std_instead_of_core = "warn"
|
||||
str_split_at_newline = "warn"
|
||||
str_to_string = "warn"
|
||||
string_add = "warn"
|
||||
string_add_assign = "warn"
|
||||
string_lit_as_bytes = "warn"
|
||||
string_lit_chars_any = "warn"
|
||||
suspicious_xor_used_as_pow = "warn"
|
||||
todo = "warn"
|
||||
too_long_first_doc_paragraph = "warn"
|
||||
too_many_lines = "allow"
|
||||
trailing_empty_array = "warn"
|
||||
trait_duplication_in_bounds = "warn"
|
||||
transmute_ptr_to_ptr = "warn"
|
||||
tuple_array_conversions = "warn"
|
||||
unchecked_time_subtraction = "warn"
|
||||
undocumented_unsafe_blocks = "warn"
|
||||
unimplemented = "warn"
|
||||
uninhabited_references = "warn"
|
||||
uninlined_format_args = "warn"
|
||||
unnecessary_box_returns = "warn"
|
||||
unnecessary_debug_formatting = "warn"
|
||||
unnecessary_literal_bound = "warn"
|
||||
unnecessary_safety_comment = "warn"
|
||||
unnecessary_safety_doc = "warn"
|
||||
unnecessary_self_imports = "warn"
|
||||
unnecessary_semicolon = "warn"
|
||||
unnecessary_struct_initialization = "warn"
|
||||
unnecessary_trailing_comma = "warn"
|
||||
unnecessary_wraps = "warn"
|
||||
unnested_or_patterns = "warn"
|
||||
unused_async = "warn"
|
||||
unused_peekable = "warn"
|
||||
unused_rounding = "warn"
|
||||
unused_self = "warn"
|
||||
unused_trait_names = "warn"
|
||||
unwrap_used = "warn"
|
||||
use_self = "warn"
|
||||
useless_let_if_seq = "warn"
|
||||
verbose_file_reads = "warn"
|
||||
wildcard_dependencies = "warn"
|
||||
wildcard_imports = "allow"
|
||||
zero_sized_map_values = "warn"
|
||||
|
||||
[lints.clippy.all]
|
||||
level = "warn"
|
||||
priority = -1
|
||||
|
||||
[lints.rust]
|
||||
trivial_casts = "allow"
|
||||
trivial_numeric_casts = "warn"
|
||||
unsafe_code = "deny"
|
||||
unused_import_braces = "warn"
|
||||
unused_lifetimes = "warn"
|
||||
unused_qualifications = "allow"
|
||||
|
||||
[lints.rust.future_incompatible]
|
||||
level = "warn"
|
||||
priority = -1
|
||||
|
||||
[lints.rust.nonstandard_style]
|
||||
level = "warn"
|
||||
priority = -1
|
||||
|
||||
[lints.rust.rust_2018_idioms]
|
||||
level = "warn"
|
||||
priority = -1
|
||||
|
||||
[lints.rustdoc]
|
||||
all = "warn"
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
[package]
|
||||
name = "egui-wgpu"
|
||||
version.workspace = true
|
||||
description = "Bindings for using egui natively using the wgpu library"
|
||||
authors = [
|
||||
"Nils Hasenbanck <nils@hasenbanck.de>",
|
||||
"embotech <opensource@embotech.com>",
|
||||
"Emil Ernerfeldt <emil.ernerfeldt@gmail.com>",
|
||||
]
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
homepage = "https://github.com/emilk/egui/tree/main/crates/egui-wgpu"
|
||||
license.workspace = true
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/emilk/egui/tree/main/crates/egui-wgpu"
|
||||
categories = ["gui", "game-development"]
|
||||
keywords = ["wgpu", "egui", "gui", "gamedev"]
|
||||
include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "**/*.wgsl", "Cargo.toml"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
[features]
|
||||
default = [
|
||||
"fragile-send-sync-non-atomic-wasm",
|
||||
"macos-window-resize-jitter-fix",
|
||||
"wgpu/default",
|
||||
"wgpu/webgl", # A very important fallback for web support
|
||||
]
|
||||
|
||||
## Enables the `capture` module for capturing screenshots.
|
||||
capture = ["dep:egui"]
|
||||
|
||||
## Enable [`winit`](https://docs.rs/winit) integration. On Linux, requires either `wayland` or `x11`
|
||||
winit = ["dep:winit", "winit/rwh_06", "dep:egui", "capture"]
|
||||
|
||||
## Enables Wayland support for winit.
|
||||
wayland = ["winit?/wayland"]
|
||||
|
||||
## Enables x11 support for winit.
|
||||
x11 = ["winit?/x11"]
|
||||
|
||||
## Make the renderer `Sync` on wasm, exploiting that by default wasm isn't multithreaded.
|
||||
## It may make code easier, especially when targeting both native and web.
|
||||
## On native most wgpu objects are send and sync, on the web they are not (by nature of the WebGPU specification).
|
||||
## This is not supported in [multithreaded WASM](https://gpuweb.github.io/gpuweb/explainer/#multithreading-transfer).
|
||||
## Thus that usage is guarded against with compiler errors in wgpu.
|
||||
fragile-send-sync-non-atomic-wasm = ["wgpu/fragile-send-sync-non-atomic-wasm"]
|
||||
|
||||
## Enables the macOS live-resize jitter fix, which uses `present_with_transaction`.
|
||||
## This requires the `wgpu/metal` backend. Disable this feature if you want to use egui-wgpu
|
||||
## with a different backend (e.g. Vulkan or GL) on macOS without pulling in Metal.
|
||||
macos-window-resize-jitter-fix = ["wgpu/metal"]
|
||||
|
||||
[dependencies]
|
||||
epaint = { workspace = true, default-features = false, features = ["bytemuck"] }
|
||||
|
||||
ahash.workspace = true
|
||||
bytemuck.workspace = true
|
||||
document-features.workspace = true
|
||||
log.workspace = true
|
||||
profiling.workspace = true
|
||||
thiserror.workspace = true
|
||||
type-map.workspace = true
|
||||
web-time.workspace = true
|
||||
wgpu = { workspace = true, features = ["wgsl"] }
|
||||
|
||||
# Optional dependencies:
|
||||
|
||||
egui = { workspace = true, optional = true, default-features = false }
|
||||
winit = { workspace = true, optional = true, default-features = false }
|
||||
Vendored
+201
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
Copyright (c) 2018-2021 Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
# egui-wgpu
|
||||
|
||||
[](https://crates.io/crates/egui-wgpu)
|
||||
[](https://docs.rs/egui-wgpu)
|
||||

|
||||

|
||||
|
||||
This crates provides bindings between [`egui`](https://github.com/emilk/egui) and [wgpu](https://crates.io/crates/wgpu).
|
||||
|
||||
This was originally hosted at https://github.com/hasenbanck/egui_wgpu_backend
|
||||
Vendored
+267
@@ -0,0 +1,267 @@
|
||||
use egui::{UserData, ViewportId};
|
||||
use epaint::ColorImage;
|
||||
use std::sync::{Arc, mpsc};
|
||||
use wgpu::{BindGroupLayout, MultisampleState, StoreOp};
|
||||
|
||||
/// A texture and a buffer for reading the rendered frame back to the cpu.
|
||||
///
|
||||
/// The texture is required since [`wgpu::TextureUsages::COPY_SRC`] is not an allowed
|
||||
/// flag for the surface texture on all platforms. This means that anytime we want to
|
||||
/// capture the frame, we first render it to this texture, and then we can copy it to
|
||||
/// both the surface texture (via a render pass) and the buffer (via a texture to buffer copy),
|
||||
/// from where we can pull it back
|
||||
/// to the cpu.
|
||||
pub struct CaptureState {
|
||||
padding: BufferPadding,
|
||||
pub texture: wgpu::Texture,
|
||||
pipeline: wgpu::RenderPipeline,
|
||||
bind_group: wgpu::BindGroup,
|
||||
}
|
||||
|
||||
pub type CaptureReceiver = mpsc::Receiver<(ViewportId, Vec<UserData>, ColorImage)>;
|
||||
pub type CaptureSender = mpsc::Sender<(ViewportId, Vec<UserData>, ColorImage)>;
|
||||
pub use mpsc::channel as capture_channel;
|
||||
|
||||
impl CaptureState {
|
||||
pub fn new(device: &wgpu::Device, surface_texture: &wgpu::Texture) -> Self {
|
||||
let shader = device.create_shader_module(wgpu::include_wgsl!("texture_copy.wgsl"));
|
||||
|
||||
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("texture_copy"),
|
||||
layout: None,
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_main"),
|
||||
compilation_options: Default::default(),
|
||||
buffers: &[],
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_main"),
|
||||
compilation_options: Default::default(),
|
||||
targets: &[Some(surface_texture.format().into())],
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: None,
|
||||
multisample: MultisampleState::default(),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
|
||||
let bind_group_layout = pipeline.get_bind_group_layout(0);
|
||||
|
||||
let (texture, padding, bind_group) =
|
||||
Self::create_texture(device, surface_texture, &bind_group_layout);
|
||||
|
||||
Self {
|
||||
padding,
|
||||
texture,
|
||||
pipeline,
|
||||
bind_group,
|
||||
}
|
||||
}
|
||||
|
||||
fn create_texture(
|
||||
device: &wgpu::Device,
|
||||
surface_texture: &wgpu::Texture,
|
||||
layout: &BindGroupLayout,
|
||||
) -> (wgpu::Texture, BufferPadding, wgpu::BindGroup) {
|
||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("egui_screen_capture_texture"),
|
||||
size: surface_texture.size(),
|
||||
mip_level_count: surface_texture.mip_level_count(),
|
||||
sample_count: surface_texture.sample_count(),
|
||||
dimension: surface_texture.dimension(),
|
||||
format: surface_texture.format(),
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
|
||||
| wgpu::TextureUsages::TEXTURE_BINDING
|
||||
| wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
});
|
||||
|
||||
let padding = BufferPadding::new(surface_texture.width());
|
||||
|
||||
let view = texture.create_view(&Default::default());
|
||||
|
||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
layout,
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(&view),
|
||||
}],
|
||||
label: None,
|
||||
});
|
||||
|
||||
(texture, padding, bind_group)
|
||||
}
|
||||
|
||||
/// Updates the [`CaptureState`] if the size of the surface texture has changed
|
||||
pub fn update(&mut self, device: &wgpu::Device, texture: &wgpu::Texture) {
|
||||
if self.texture.size() != texture.size() {
|
||||
let (new_texture, padding, bind_group) =
|
||||
Self::create_texture(device, texture, &self.pipeline.get_bind_group_layout(0));
|
||||
self.texture = new_texture;
|
||||
self.padding = padding;
|
||||
self.bind_group = bind_group;
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles copying from the [`CaptureState`] texture to the surface texture and the buffer.
|
||||
/// Pass the returned buffer to [`CaptureState::read_screen_rgba`] to read the data back to the cpu.
|
||||
pub fn copy_textures(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
output_frame: &wgpu::SurfaceTexture,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
) -> wgpu::Buffer {
|
||||
debug_assert_eq!(
|
||||
self.texture.size(),
|
||||
output_frame.texture.size(),
|
||||
"Texture sizes must match, `CaptureState::update` was probably not called"
|
||||
);
|
||||
|
||||
// It would be more efficient to reuse the Buffer, e.g. via some kind of ring buffer, but
|
||||
// for most screenshot use cases this should be fine. When taking many screenshots (e.g. for a video)
|
||||
// it might make sense to revisit this and implement a more efficient solution.
|
||||
#[allow(clippy::allow_attributes, clippy::arc_with_non_send_sync)] // For wasm
|
||||
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("egui_screen_capture_buffer"),
|
||||
size: (self.padding.padded_bytes_per_row * self.texture.height()) as u64,
|
||||
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let padding = self.padding;
|
||||
let tex = &mut self.texture;
|
||||
|
||||
let tex_extent = tex.size();
|
||||
|
||||
encoder.copy_texture_to_buffer(
|
||||
tex.as_image_copy(),
|
||||
wgpu::TexelCopyBufferInfo {
|
||||
buffer: &buffer,
|
||||
layout: wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(padding.padded_bytes_per_row),
|
||||
rows_per_image: None,
|
||||
},
|
||||
},
|
||||
tex_extent,
|
||||
);
|
||||
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("texture_copy"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &output_frame.texture.create_view(&Default::default()),
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
|
||||
store: StoreOp::Store,
|
||||
},
|
||||
depth_slice: None,
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
occlusion_query_set: None,
|
||||
timestamp_writes: None,
|
||||
multiview_mask: None,
|
||||
});
|
||||
|
||||
pass.set_pipeline(&self.pipeline);
|
||||
pass.set_bind_group(0, &self.bind_group, &[]);
|
||||
pass.draw(0..3, 0..1);
|
||||
|
||||
buffer
|
||||
}
|
||||
|
||||
/// Handles copying from the [`CaptureState`] texture to the surface texture and the cpu
|
||||
/// This function is non-blocking and will send the data to the given sender when it's ready.
|
||||
/// Pass in the buffer returned from [`CaptureState::copy_textures`].
|
||||
/// Make sure to call this after the encoder has been submitted.
|
||||
pub fn read_screen_rgba(
|
||||
&self,
|
||||
ctx: egui::Context,
|
||||
buffer: wgpu::Buffer,
|
||||
data: Vec<UserData>,
|
||||
tx: CaptureSender,
|
||||
viewport_id: ViewportId,
|
||||
) {
|
||||
#[allow(clippy::allow_attributes, clippy::arc_with_non_send_sync)] // For wasm
|
||||
let buffer = Arc::new(buffer);
|
||||
let buffer_clone = Arc::clone(&buffer);
|
||||
let buffer_slice = buffer_clone.slice(..);
|
||||
let format = self.texture.format();
|
||||
let tex_extent = self.texture.size();
|
||||
let padding = self.padding;
|
||||
let to_rgba = match format {
|
||||
wgpu::TextureFormat::Rgba8Unorm => [0, 1, 2, 3],
|
||||
wgpu::TextureFormat::Bgra8Unorm => [2, 1, 0, 3],
|
||||
_ => {
|
||||
log::error!(
|
||||
"Screen can't be captured unless the surface format is Rgba8Unorm or Bgra8Unorm. Current surface format is {format:?}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
|
||||
if let Err(err) = result {
|
||||
log::error!("Failed to map buffer for reading: {err}");
|
||||
return;
|
||||
}
|
||||
let buffer_slice = buffer.slice(..);
|
||||
|
||||
let mut pixels = Vec::with_capacity((tex_extent.width * tex_extent.height) as usize);
|
||||
let mapped_range = match buffer_slice.get_mapped_range() {
|
||||
Ok(range) => range,
|
||||
Err(err) => {
|
||||
log::error!("Failed to get mapped range for reading: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
for padded_row in mapped_range.chunks(padding.padded_bytes_per_row as usize) {
|
||||
let row = &padded_row[..padding.unpadded_bytes_per_row as usize];
|
||||
for color in row.chunks(4) {
|
||||
pixels.push(epaint::Color32::from_rgba_premultiplied(
|
||||
color[to_rgba[0]],
|
||||
color[to_rgba[1]],
|
||||
color[to_rgba[2]],
|
||||
color[to_rgba[3]],
|
||||
));
|
||||
}
|
||||
}
|
||||
drop(mapped_range);
|
||||
buffer.unmap();
|
||||
|
||||
tx.send((
|
||||
viewport_id,
|
||||
data,
|
||||
ColorImage::new(
|
||||
[tex_extent.width as usize, tex_extent.height as usize],
|
||||
pixels,
|
||||
),
|
||||
))
|
||||
.ok();
|
||||
ctx.request_repaint();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct BufferPadding {
|
||||
unpadded_bytes_per_row: u32,
|
||||
padded_bytes_per_row: u32,
|
||||
}
|
||||
|
||||
impl BufferPadding {
|
||||
fn new(width: u32) -> Self {
|
||||
let bytes_per_pixel = core::mem::size_of::<u32>() as u32;
|
||||
let unpadded_bytes_per_row = width * bytes_per_pixel;
|
||||
let padded_bytes_per_row =
|
||||
wgpu::util::align_to(unpadded_bytes_per_row, wgpu::COPY_BYTES_PER_ROW_ALIGNMENT);
|
||||
Self {
|
||||
unpadded_bytes_per_row,
|
||||
padded_bytes_per_row,
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+162
@@ -0,0 +1,162 @@
|
||||
// Vertex shader bindings
|
||||
|
||||
struct VertexOutput {
|
||||
@location(0) tex_coord: vec2<f32>,
|
||||
@location(1) color: vec4<f32>, // gamma 0-1
|
||||
@builtin(position) position: vec4<f32>,
|
||||
};
|
||||
|
||||
struct Locals {
|
||||
screen_size: vec2<f32>,
|
||||
|
||||
/// 1 if dithering is enabled, 0 otherwise
|
||||
dithering: u32,
|
||||
|
||||
/// 1 to do manual filtering for more predictable kittest snapshot images.
|
||||
/// See also https://github.com/emilk/egui/issues/5295
|
||||
predictable_texture_filtering: u32,
|
||||
};
|
||||
@group(0) @binding(0) var<uniform> r_locals: Locals;
|
||||
|
||||
|
||||
// -----------------------------------------------
|
||||
// Adapted from
|
||||
// https://www.shadertoy.com/view/llVGzG
|
||||
// Originally presented in:
|
||||
// Jimenez 2014, "Next Generation Post-Processing in Call of Duty"
|
||||
//
|
||||
// A good overview can be found in
|
||||
// https://blog.demofox.org/2022/01/01/interleaved-gradient-noise-a-different-kind-of-low-discrepancy-sequence/
|
||||
// via https://github.com/rerun-io/rerun/
|
||||
fn interleaved_gradient_noise(n: vec2<f32>) -> f32 {
|
||||
let f = 0.06711056 * n.x + 0.00583715 * n.y;
|
||||
return fract(52.9829189 * fract(f));
|
||||
}
|
||||
|
||||
fn dither_interleaved(rgb: vec3<f32>, levels: f32, frag_coord: vec4<f32>) -> vec3<f32> {
|
||||
var noise = interleaved_gradient_noise(frag_coord.xy);
|
||||
// scale down the noise slightly to ensure flat colors aren't getting dithered
|
||||
noise = (noise - 0.5) * 0.95;
|
||||
return rgb + noise / (levels - 1.0);
|
||||
}
|
||||
|
||||
// 0-1 linear from 0-1 sRGB gamma
|
||||
fn linear_from_gamma_rgb(srgb: vec3<f32>) -> vec3<f32> {
|
||||
let cutoff = srgb < vec3<f32>(0.04045);
|
||||
let lower = srgb / vec3<f32>(12.92);
|
||||
let higher = pow((srgb + vec3<f32>(0.055)) / vec3<f32>(1.055), vec3<f32>(2.4));
|
||||
return select(higher, lower, cutoff);
|
||||
}
|
||||
|
||||
// 0-1 sRGB gamma from 0-1 linear
|
||||
fn gamma_from_linear_rgb(rgb: vec3<f32>) -> vec3<f32> {
|
||||
let cutoff = rgb < vec3<f32>(0.0031308);
|
||||
let lower = rgb * vec3<f32>(12.92);
|
||||
let higher = vec3<f32>(1.055) * pow(rgb, vec3<f32>(1.0 / 2.4)) - vec3<f32>(0.055);
|
||||
return select(higher, lower, cutoff);
|
||||
}
|
||||
|
||||
// 0-1 sRGBA gamma from 0-1 linear
|
||||
fn gamma_from_linear_rgba(linear_rgba: vec4<f32>) -> vec4<f32> {
|
||||
return vec4<f32>(gamma_from_linear_rgb(linear_rgba.rgb), linear_rgba.a);
|
||||
}
|
||||
|
||||
// [u8; 4] SRGB as u32 -> [r, g, b, a] in 0.-1
|
||||
fn unpack_color(color: u32) -> vec4<f32> {
|
||||
return vec4<f32>(
|
||||
f32(color & 255u),
|
||||
f32((color >> 8u) & 255u),
|
||||
f32((color >> 16u) & 255u),
|
||||
f32((color >> 24u) & 255u),
|
||||
) / 255.0;
|
||||
}
|
||||
|
||||
fn position_from_screen(screen_pos: vec2<f32>) -> vec4<f32> {
|
||||
return vec4<f32>(
|
||||
2.0 * screen_pos.x / r_locals.screen_size.x - 1.0,
|
||||
1.0 - 2.0 * screen_pos.y / r_locals.screen_size.y,
|
||||
0.0,
|
||||
1.0,
|
||||
);
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_main(
|
||||
@location(0) a_pos: vec2<f32>,
|
||||
@location(1) a_tex_coord: vec2<f32>,
|
||||
@location(2) a_color: u32,
|
||||
) -> VertexOutput {
|
||||
var out: VertexOutput;
|
||||
out.tex_coord = a_tex_coord;
|
||||
out.color = unpack_color(a_color);
|
||||
out.position = position_from_screen(a_pos);
|
||||
return out;
|
||||
}
|
||||
|
||||
// Fragment shader bindings
|
||||
|
||||
@group(1) @binding(0) var r_tex_color: texture_2d<f32>;
|
||||
@group(1) @binding(1) var r_tex_sampler: sampler;
|
||||
|
||||
fn sample_texture(in: VertexOutput) -> vec4<f32> {
|
||||
if r_locals.predictable_texture_filtering == 0 {
|
||||
// Hardware filtering: fast, but varies across GPUs and drivers.
|
||||
return textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
|
||||
} else {
|
||||
// Manual bilinear filtering with four taps at pixel centers using textureLoad
|
||||
let texture_size = vec2<i32>(textureDimensions(r_tex_color, 0));
|
||||
let texture_size_f = vec2<f32>(texture_size);
|
||||
let pixel_coord = in.tex_coord * texture_size_f - 0.5;
|
||||
let pixel_fract = fract(pixel_coord);
|
||||
let pixel_floor = vec2<i32>(floor(pixel_coord));
|
||||
|
||||
// Manual texture clamping
|
||||
let max_coord = texture_size - vec2<i32>(1, 1);
|
||||
let p00 = clamp(pixel_floor + vec2<i32>(0, 0), vec2<i32>(0, 0), max_coord);
|
||||
let p10 = clamp(pixel_floor + vec2<i32>(1, 0), vec2<i32>(0, 0), max_coord);
|
||||
let p01 = clamp(pixel_floor + vec2<i32>(0, 1), vec2<i32>(0, 0), max_coord);
|
||||
let p11 = clamp(pixel_floor + vec2<i32>(1, 1), vec2<i32>(0, 0), max_coord);
|
||||
|
||||
// Load at pixel centers
|
||||
let tl = textureLoad(r_tex_color, p00, 0);
|
||||
let tr = textureLoad(r_tex_color, p10, 0);
|
||||
let bl = textureLoad(r_tex_color, p01, 0);
|
||||
let br = textureLoad(r_tex_color, p11, 0);
|
||||
|
||||
// Manual bilinear interpolation
|
||||
let top = mix(tl, tr, pixel_fract.x);
|
||||
let bottom = mix(bl, br, pixel_fract.x);
|
||||
return mix(top, bottom, pixel_fract.y);
|
||||
}
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main_linear_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
// We expect "normal" textures that are NOT sRGB-aware.
|
||||
let tex_gamma = sample_texture(in);
|
||||
var out_color_gamma = in.color * tex_gamma;
|
||||
// Dither the float color down to eight bits to reduce banding.
|
||||
// This step is optional for egui backends.
|
||||
// Note that dithering is performed on the gamma encoded values,
|
||||
// because this function is used together with a srgb converting target.
|
||||
if r_locals.dithering == 1 {
|
||||
let out_color_gamma_rgb = dither_interleaved(out_color_gamma.rgb, 256.0, in.position);
|
||||
out_color_gamma = vec4<f32>(out_color_gamma_rgb, out_color_gamma.a);
|
||||
}
|
||||
let out_color_linear = linear_from_gamma_rgb(out_color_gamma.rgb);
|
||||
return vec4<f32>(out_color_linear, out_color_gamma.a);
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main_gamma_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
// We expect "normal" textures that are NOT sRGB-aware.
|
||||
let tex_gamma = sample_texture(in);
|
||||
var out_color_gamma = in.color * tex_gamma;
|
||||
// Dither the float color down to eight bits to reduce banding.
|
||||
// This step is optional for egui backends.
|
||||
if r_locals.dithering == 1 {
|
||||
let out_color_gamma_rgb = dither_interleaved(out_color_gamma.rgb, 256.0, in.position);
|
||||
out_color_gamma = vec4<f32>(out_color_gamma_rgb, out_color_gamma.a);
|
||||
}
|
||||
return out_color_gamma;
|
||||
}
|
||||
Vendored
+555
@@ -0,0 +1,555 @@
|
||||
//! This crates provides bindings between [`egui`](https://github.com/emilk/egui) and [wgpu](https://crates.io/crates/wgpu).
|
||||
//!
|
||||
//! If you're targeting WebGL you also need to turn on the
|
||||
//! `webgl` feature of the `wgpu` crate:
|
||||
//!
|
||||
//! ```toml
|
||||
//! # Enable both WebGL and WebGPU backends on web.
|
||||
//! wgpu = { version = "*", features = ["webgpu", "webgl"] }
|
||||
//! ```
|
||||
//!
|
||||
//! You can control whether WebGL or WebGPU will be picked at runtime by configuring
|
||||
//! [`WgpuConfiguration::wgpu_setup`].
|
||||
//! The default is to prefer WebGPU and fall back on WebGL.
|
||||
//!
|
||||
//! ## Feature flags
|
||||
#![doc = document_features::document_features!()]
|
||||
//!
|
||||
|
||||
pub use wgpu;
|
||||
|
||||
/// Low-level painting of [`egui`](https://github.com/emilk/egui) on [`wgpu`].
|
||||
mod renderer;
|
||||
|
||||
mod setup;
|
||||
|
||||
pub use renderer::*;
|
||||
pub use setup::{
|
||||
EguiDisplayHandle, NativeAdapterSelectorMethod, WgpuSetup, WgpuSetupCreateNew,
|
||||
WgpuSetupExisting,
|
||||
};
|
||||
|
||||
/// Helpers for capturing screenshots of the UI.
|
||||
#[cfg(feature = "capture")]
|
||||
pub mod capture;
|
||||
|
||||
/// Module for painting [`egui`](https://github.com/emilk/egui) with [`wgpu`] on [`winit`].
|
||||
#[cfg(feature = "winit")]
|
||||
pub mod winit;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use epaint::mutex::RwLock;
|
||||
|
||||
/// An error produced by egui-wgpu.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum WgpuError {
|
||||
#[error(transparent)]
|
||||
RequestAdapterError(#[from] wgpu::RequestAdapterError),
|
||||
|
||||
#[error("Adapter selection failed: {0}")]
|
||||
CustomNativeAdapterSelectionError(String),
|
||||
|
||||
#[error("There was no valid format for the surface at all.")]
|
||||
NoSurfaceFormatsAvailable,
|
||||
|
||||
#[error(transparent)]
|
||||
RequestDeviceError(#[from] wgpu::RequestDeviceError),
|
||||
|
||||
#[error(transparent)]
|
||||
CreateSurfaceError(#[from] wgpu::CreateSurfaceError),
|
||||
|
||||
#[cfg(feature = "winit")]
|
||||
#[error(transparent)]
|
||||
HandleError(#[from] ::winit::raw_window_handle::HandleError),
|
||||
}
|
||||
|
||||
/// Runtime-mutable subset of [`WgpuConfiguration`].
|
||||
///
|
||||
/// Edit any field to have the surface reconfigured on the next paint.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct SurfaceConfig {
|
||||
/// Present mode used for the primary surface.
|
||||
pub present_mode: wgpu::PresentMode,
|
||||
|
||||
/// Desired maximum number of frames that the presentation engine should queue in advance.
|
||||
///
|
||||
/// Use `1` for low-latency, and `2` for high-throughput.
|
||||
///
|
||||
/// See [`wgpu::SurfaceConfiguration::desired_maximum_frame_latency`] for details.
|
||||
///
|
||||
/// `None` => Let `wgpu` pick a default (currently `2`).
|
||||
pub desired_maximum_frame_latency: Option<u32>,
|
||||
}
|
||||
|
||||
impl SurfaceConfig {
|
||||
/// Good default for GUIs with very little (or no) extra GPU work.
|
||||
pub const LOW_LATENCY: Self = Self {
|
||||
present_mode: wgpu::PresentMode::AutoVsync,
|
||||
|
||||
desired_maximum_frame_latency: if cfg!(target_os = "ios") {
|
||||
None // The default is good on iOS, while `Some(1)` cuts FPS in half
|
||||
} else {
|
||||
Some(1)
|
||||
},
|
||||
};
|
||||
|
||||
/// Good default for GUIs with a lot of extra GPU work,
|
||||
/// or that want to prioritize smoothness over latency.
|
||||
pub const HIGH_THROUGHPUT: Self = Self {
|
||||
present_mode: wgpu::PresentMode::AutoVsync,
|
||||
desired_maximum_frame_latency: Some(2), // High-throughput.
|
||||
};
|
||||
}
|
||||
|
||||
/// Access to the render state for egui.
|
||||
#[derive(Clone)]
|
||||
pub struct RenderState {
|
||||
/// Wgpu adapter used for rendering.
|
||||
pub adapter: wgpu::Adapter,
|
||||
|
||||
/// All the available adapters.
|
||||
///
|
||||
/// This is not available on web.
|
||||
/// On web, we always select WebGPU is available, then fall back to WebGL if not.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub available_adapters: Vec<wgpu::Adapter>,
|
||||
|
||||
/// Wgpu instance used for creating surfaces and adapters.
|
||||
pub instance: wgpu::Instance,
|
||||
|
||||
/// Wgpu device used for rendering, created from the adapter.
|
||||
pub device: wgpu::Device,
|
||||
|
||||
/// Wgpu queue used for rendering, created from the adapter.
|
||||
pub queue: wgpu::Queue,
|
||||
|
||||
/// The target texture format used for presenting to the window.
|
||||
pub target_format: wgpu::TextureFormat,
|
||||
|
||||
/// Egui renderer responsible for drawing the UI.
|
||||
pub renderer: Arc<RwLock<Renderer>>,
|
||||
|
||||
/// Runtime-mutable subset of the wgpu configuration.
|
||||
///
|
||||
/// Update this to have the surface reconfigured on the next paint.
|
||||
pub surface_config: SurfaceConfig,
|
||||
}
|
||||
|
||||
async fn request_adapter(
|
||||
instance: &wgpu::Instance,
|
||||
power_preference: wgpu::PowerPreference,
|
||||
compatible_surface: Option<&wgpu::Surface<'_>>,
|
||||
available_adapters: &[wgpu::Adapter],
|
||||
) -> Result<wgpu::Adapter, WgpuError> {
|
||||
profiling::function_scope!();
|
||||
|
||||
let adapter = instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
power_preference,
|
||||
compatible_surface,
|
||||
// We don't expose this as an option right now since it's fairly rarely useful:
|
||||
// * only has an effect on native
|
||||
// * fails if there's no software rasterizer available
|
||||
// * can achieve the same with `native_adapter_selector`
|
||||
force_fallback_adapter: false,
|
||||
apply_limit_buckets: false,
|
||||
})
|
||||
.await
|
||||
.inspect_err(|_err| {
|
||||
if cfg!(target_arch = "wasm32") {
|
||||
// Nothing to add here
|
||||
} else if available_adapters.is_empty() {
|
||||
if std::env::var("DYLD_LIBRARY_PATH").is_ok() {
|
||||
// DYLD_LIBRARY_PATH can sometimes lead to loading dylibs that cause
|
||||
// us to find zero adapters. Very strange.
|
||||
// I don't want to debug this again.
|
||||
// See https://github.com/rerun-io/rerun/issues/11351 for more
|
||||
log::warn!(
|
||||
"No wgpu adapter found. This could be because DYLD_LIBRARY_PATH causes dylibs to be loaded that interfere with Metal device creation. Try restarting with DYLD_LIBRARY_PATH=''"
|
||||
);
|
||||
} else {
|
||||
log::info!("No wgpu adapter found");
|
||||
}
|
||||
} else if available_adapters.len() == 1 {
|
||||
log::info!(
|
||||
"The only available wgpu adapter was not suitable: {}",
|
||||
adapter_info_summary(&available_adapters[0].get_info())
|
||||
);
|
||||
} else {
|
||||
log::info!(
|
||||
"No suitable wgpu adapter found out of the {} available ones: {}",
|
||||
available_adapters.len(),
|
||||
describe_adapters(available_adapters)
|
||||
);
|
||||
}
|
||||
})?;
|
||||
|
||||
if 1 < available_adapters.len() {
|
||||
log::info!(
|
||||
"There are {} available wgpu adapters: {}",
|
||||
available_adapters.len(),
|
||||
describe_adapters(available_adapters)
|
||||
);
|
||||
}
|
||||
|
||||
Ok(adapter)
|
||||
}
|
||||
|
||||
impl RenderState {
|
||||
/// Creates a new [`RenderState`], containing everything needed for drawing egui with wgpu.
|
||||
///
|
||||
/// # Errors
|
||||
/// Wgpu initialization may fail due to incompatible hardware or driver for a given config.
|
||||
pub async fn create(
|
||||
config: &WgpuConfiguration,
|
||||
instance: &wgpu::Instance,
|
||||
compatible_surface: Option<&wgpu::Surface<'static>>,
|
||||
options: RendererOptions,
|
||||
) -> Result<Self, WgpuError> {
|
||||
profiling::scope!("RenderState::create"); // async yield give bad names using `profile_function`
|
||||
|
||||
// This is always an empty list on web.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let available_adapters = {
|
||||
let backends = if let WgpuSetup::CreateNew(create_new) = &config.wgpu_setup {
|
||||
create_new.instance_descriptor.backends
|
||||
} else {
|
||||
wgpu::Backends::all()
|
||||
};
|
||||
|
||||
instance.enumerate_adapters(backends).await
|
||||
};
|
||||
|
||||
let (instance, adapter, device, queue) = match config.wgpu_setup.clone() {
|
||||
WgpuSetup::CreateNew(WgpuSetupCreateNew {
|
||||
instance_descriptor: _,
|
||||
display_handle: _,
|
||||
power_preference,
|
||||
native_adapter_selector: _native_adapter_selector,
|
||||
device_descriptor,
|
||||
}) => {
|
||||
let adapter = {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
request_adapter(instance, power_preference, compatible_surface, &[]).await
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let Some(native_adapter_selector) = _native_adapter_selector {
|
||||
native_adapter_selector(&available_adapters, compatible_surface)
|
||||
.map_err(WgpuError::CustomNativeAdapterSelectionError)
|
||||
} else {
|
||||
request_adapter(
|
||||
instance,
|
||||
power_preference,
|
||||
compatible_surface,
|
||||
&available_adapters,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}?;
|
||||
|
||||
let (device, queue) = {
|
||||
profiling::scope!("request_device");
|
||||
adapter
|
||||
.request_device(&(*device_descriptor)(&adapter))
|
||||
.await?
|
||||
};
|
||||
|
||||
(instance.clone(), adapter, device, queue)
|
||||
}
|
||||
WgpuSetup::Existing(WgpuSetupExisting {
|
||||
instance,
|
||||
adapter,
|
||||
device,
|
||||
queue,
|
||||
}) => (instance, adapter, device, queue),
|
||||
};
|
||||
|
||||
log_adapter_info(&adapter.get_info());
|
||||
|
||||
let surface_formats = {
|
||||
profiling::scope!("get_capabilities");
|
||||
compatible_surface.map_or_else(
|
||||
|| vec![wgpu::TextureFormat::Rgba8Unorm],
|
||||
|s| s.get_capabilities(&adapter).formats,
|
||||
)
|
||||
};
|
||||
let target_format = crate::preferred_framebuffer_format(&surface_formats)?;
|
||||
|
||||
let renderer = Renderer::new(&device, target_format, options);
|
||||
|
||||
// On wasm, depending on feature flags, wgpu objects may or may not implement sync.
|
||||
// It doesn't make sense to switch to Rc for that special usecase, so simply disable the lint.
|
||||
#[allow(clippy::allow_attributes, clippy::arc_with_non_send_sync)] // For wasm
|
||||
Ok(Self {
|
||||
instance,
|
||||
adapter,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
available_adapters,
|
||||
device,
|
||||
queue,
|
||||
target_format,
|
||||
renderer: Arc::new(RwLock::new(renderer)),
|
||||
surface_config: config.surface,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn describe_adapters(adapters: &[wgpu::Adapter]) -> String {
|
||||
if adapters.is_empty() {
|
||||
"(none)".to_owned()
|
||||
} else if adapters.len() == 1 {
|
||||
adapter_info_summary(&adapters[0].get_info())
|
||||
} else {
|
||||
adapters
|
||||
.iter()
|
||||
.map(|a| format!("{{{}}}", adapter_info_summary(&a.get_info())))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
}
|
||||
|
||||
/// Specifies which action should be taken as consequence of a surface error.
|
||||
pub enum SurfaceErrorAction {
|
||||
/// Do nothing and skip the current frame.
|
||||
SkipFrame,
|
||||
|
||||
/// Reconfigure the existing surface, then skip the current frame.
|
||||
///
|
||||
/// Calls [`wgpu::Surface::configure`] on the current surface object.
|
||||
/// Use for [`wgpu::CurrentSurfaceTexture::Outdated`].
|
||||
Reconfigure,
|
||||
|
||||
/// Drop the surface, create a new one via [`wgpu::Instance::create_surface`], configure it,
|
||||
/// then skip the current frame.
|
||||
///
|
||||
/// Use for [`wgpu::CurrentSurfaceTexture::Lost`], where reconfiguring the same surface
|
||||
/// object cannot recover.
|
||||
RecreateSurface,
|
||||
}
|
||||
|
||||
/// Configuration for using wgpu with eframe or the egui-wgpu winit feature.
|
||||
#[derive(Clone)]
|
||||
pub struct WgpuConfiguration {
|
||||
/// Runtime-mutable configuration for the surface (present mode, frame latency).
|
||||
///
|
||||
/// These are the fields exposed via [`RenderState::surface_config`] for live
|
||||
/// reconfiguration at runtime.
|
||||
pub surface: SurfaceConfig,
|
||||
|
||||
/// How to create the wgpu adapter & device
|
||||
pub wgpu_setup: WgpuSetup,
|
||||
|
||||
/// Callback for surface status changes.
|
||||
///
|
||||
/// Called with the [`wgpu::CurrentSurfaceTexture`] result whenever acquiring a frame
|
||||
/// does not return [`wgpu::CurrentSurfaceTexture::Success`]. For
|
||||
/// [`wgpu::CurrentSurfaceTexture::Suboptimal`], egui uses the frame as-is and
|
||||
/// defers surface reconfiguration to the next frame — the callback is not invoked
|
||||
/// in that case either.
|
||||
pub on_surface_status:
|
||||
Arc<dyn Fn(&wgpu::CurrentSurfaceTexture) -> SurfaceErrorAction + Send + Sync>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wgpu_config_impl_send_sync() {
|
||||
fn assert_send_sync<T: Send + Sync>() {}
|
||||
assert_send_sync::<WgpuConfiguration>();
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for WgpuConfiguration {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
let Self {
|
||||
surface,
|
||||
wgpu_setup,
|
||||
on_surface_status: _,
|
||||
} = self;
|
||||
f.debug_struct("WgpuConfiguration")
|
||||
.field("surface", &surface)
|
||||
.field("wgpu_setup", &wgpu_setup)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl WgpuConfiguration {
|
||||
#[inline]
|
||||
pub fn with_surface_config(mut self, surface_config: SurfaceConfig) -> Self {
|
||||
self.surface = surface_config;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for WgpuConfiguration {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
surface: SurfaceConfig::HIGH_THROUGHPUT,
|
||||
|
||||
// No display handle available at this point — callers should replace this with
|
||||
// `WgpuSetup::from_display_handle(...)` before creating the instance if one is available.
|
||||
wgpu_setup: WgpuSetup::without_display_handle(),
|
||||
on_surface_status: Arc::new(|status| match status {
|
||||
wgpu::CurrentSurfaceTexture::Outdated => {
|
||||
// The compositor changed the surface (resize, scale, output, …). wgpu
|
||||
// requires us to reconfigure before the next acquire. Skipping would mean
|
||||
// we are stuck in `Outdated` forever.
|
||||
log::trace!("Dropped frame with error: {status:?}");
|
||||
SurfaceErrorAction::Reconfigure
|
||||
}
|
||||
wgpu::CurrentSurfaceTexture::Lost => {
|
||||
// The underlying surface is gone and we need a fresh one from the `wgpu::Instance`.
|
||||
log::debug!("Dropped frame with error: {status:?}");
|
||||
SurfaceErrorAction::RecreateSurface
|
||||
}
|
||||
wgpu::CurrentSurfaceTexture::Occluded => {
|
||||
// App is hidden (minimized / behind another window). Skip silently.
|
||||
log::trace!("Skipping frame due to occlusion.");
|
||||
SurfaceErrorAction::SkipFrame
|
||||
}
|
||||
_ => {
|
||||
log::warn!("Dropped frame with error: {status:?}");
|
||||
SurfaceErrorAction::SkipFrame
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the framebuffer format that egui prefers
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`WgpuError::NoSurfaceFormatsAvailable`] if the given list of formats is empty.
|
||||
pub fn preferred_framebuffer_format(
|
||||
formats: &[wgpu::TextureFormat],
|
||||
) -> Result<wgpu::TextureFormat, WgpuError> {
|
||||
for &format in formats {
|
||||
if matches!(
|
||||
format,
|
||||
wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Bgra8Unorm
|
||||
) {
|
||||
return Ok(format);
|
||||
}
|
||||
}
|
||||
|
||||
formats
|
||||
.first()
|
||||
.copied()
|
||||
.ok_or(WgpuError::NoSurfaceFormatsAvailable)
|
||||
}
|
||||
|
||||
/// Take's epi's depth/stencil bits and returns the corresponding wgpu format.
|
||||
pub fn depth_format_from_bits(depth_buffer: u8, stencil_buffer: u8) -> Option<wgpu::TextureFormat> {
|
||||
match (depth_buffer, stencil_buffer) {
|
||||
(0, 8) => Some(wgpu::TextureFormat::Stencil8),
|
||||
(16, 0) => Some(wgpu::TextureFormat::Depth16Unorm),
|
||||
(24, 0) => Some(wgpu::TextureFormat::Depth24Plus),
|
||||
(24, 8) => Some(wgpu::TextureFormat::Depth24PlusStencil8),
|
||||
(32, 0) => Some(wgpu::TextureFormat::Depth32Float),
|
||||
(32, 8) => Some(wgpu::TextureFormat::Depth32FloatStencil8),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn log_adapter_info(info: &wgpu::AdapterInfo) {
|
||||
let summary = adapter_info_summary(info);
|
||||
|
||||
let is_test = cfg!(test); // Software rasterizers are expected (and preferred) during testing!
|
||||
|
||||
if info.device_type == wgpu::DeviceType::Cpu && !is_test {
|
||||
log::warn!("Software rasterizer detected - loss of performance expected. {summary}");
|
||||
} else {
|
||||
log::debug!("wgpu adapter: {summary}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A human-readable summary about an adapter
|
||||
pub fn adapter_info_summary(info: &wgpu::AdapterInfo) -> String {
|
||||
let wgpu::AdapterInfo {
|
||||
name,
|
||||
vendor,
|
||||
device,
|
||||
device_type,
|
||||
driver,
|
||||
driver_info,
|
||||
backend,
|
||||
device_pci_bus_id,
|
||||
subgroup_min_size,
|
||||
subgroup_max_size,
|
||||
transient_saves_memory,
|
||||
limit_bucket,
|
||||
} = &info;
|
||||
|
||||
// Example values:
|
||||
// > name: "llvmpipe (LLVM 16.0.6, 256 bits)", device_type: Cpu, backend: Vulkan, driver: "llvmpipe", driver_info: "Mesa 23.1.6-arch1.4 (LLVM 16.0.6)"
|
||||
// > name: "Apple M1 Pro", device_type: IntegratedGpu, backend: Metal, driver: "", driver_info: ""
|
||||
// > name: "ANGLE (Apple, Apple M1 Pro, OpenGL 4.1)", device_type: IntegratedGpu, backend: Gl, driver: "", driver_info: ""
|
||||
|
||||
use core::fmt::Write as _;
|
||||
|
||||
let mut summary = format!("backend: {backend:?}, device_type: {device_type:?}");
|
||||
|
||||
if !name.is_empty() {
|
||||
write!(summary, ", name: {name:?}").ok();
|
||||
}
|
||||
if !driver.is_empty() {
|
||||
write!(summary, ", driver: {driver:?}").ok();
|
||||
}
|
||||
if !driver_info.is_empty() {
|
||||
write!(summary, ", driver_info: {driver_info:?}").ok();
|
||||
}
|
||||
if *vendor != 0 {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
write!(
|
||||
summary,
|
||||
", vendor: {} (0x{vendor:04X})",
|
||||
parse_vendor_id(*vendor)
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
write!(summary, ", vendor: 0x{vendor:04X}").ok();
|
||||
}
|
||||
}
|
||||
if *device != 0 {
|
||||
write!(summary, ", device: 0x{device:02X}").ok();
|
||||
}
|
||||
if !device_pci_bus_id.is_empty() {
|
||||
write!(summary, ", pci_bus_id: {device_pci_bus_id:?}").ok();
|
||||
}
|
||||
if *subgroup_min_size != 0 || *subgroup_max_size != 0 {
|
||||
write!(
|
||||
summary,
|
||||
", subgroup_size: {subgroup_min_size}..={subgroup_max_size}"
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
write!(
|
||||
summary,
|
||||
", transient_saves_memory: {transient_saves_memory:?}"
|
||||
)
|
||||
.ok();
|
||||
write!(summary, ", limit_bucket: {limit_bucket:?}").ok();
|
||||
|
||||
summary
|
||||
}
|
||||
|
||||
/// Tries to parse the adapter's vendor ID to a human-readable string.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn parse_vendor_id(vendor_id: u32) -> &'static str {
|
||||
match vendor_id {
|
||||
wgpu::hal::auxil::db::amd::VENDOR => "AMD",
|
||||
wgpu::hal::auxil::db::apple::VENDOR => "Apple",
|
||||
wgpu::hal::auxil::db::arm::VENDOR => "ARM",
|
||||
wgpu::hal::auxil::db::broadcom::VENDOR => "Broadcom",
|
||||
wgpu::hal::auxil::db::imgtec::VENDOR => "Imagination Technologies",
|
||||
wgpu::hal::auxil::db::intel::VENDOR => "Intel",
|
||||
wgpu::hal::auxil::db::mesa::VENDOR => "Mesa",
|
||||
wgpu::hal::auxil::db::nvidia::VENDOR => "NVIDIA",
|
||||
wgpu::hal::auxil::db::qualcomm::VENDOR => "Qualcomm",
|
||||
_ => "Unknown",
|
||||
}
|
||||
}
|
||||
Vendored
+1178
File diff suppressed because it is too large
Load Diff
Vendored
+331
@@ -0,0 +1,331 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A cloneable display handle for use with [`wgpu::InstanceDescriptor`].
|
||||
///
|
||||
/// [`wgpu::InstanceDescriptor`] stores its display handle as a non-cloneable
|
||||
/// `Box<dyn WgpuHasDisplayHandle>`. This trait wraps it so it can be cloned
|
||||
/// alongside the rest of the egui wgpu configuration.
|
||||
///
|
||||
/// Automatically implemented for all types that satisfy the bounds
|
||||
/// (including [`winit::event_loop::OwnedDisplayHandle`]).
|
||||
pub trait EguiDisplayHandle:
|
||||
wgpu::rwh::HasDisplayHandle + core::fmt::Debug + Send + Sync + 'static
|
||||
{
|
||||
/// Clone into a `Box<dyn WgpuHasDisplayHandle>` for [`wgpu::InstanceDescriptor::display`].
|
||||
fn clone_for_wgpu(&self) -> Box<dyn wgpu::wgt::WgpuHasDisplayHandle>;
|
||||
|
||||
/// Clone into a new `Box<dyn EguiDisplayHandle>`.
|
||||
fn clone_display_handle(&self) -> Box<dyn EguiDisplayHandle>;
|
||||
}
|
||||
|
||||
impl Clone for Box<dyn EguiDisplayHandle> {
|
||||
fn clone(&self) -> Self {
|
||||
// We need to deref here, otherwise this causes infinite recursion stack overflow.
|
||||
(**self).clone_display_handle()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> EguiDisplayHandle for T
|
||||
where
|
||||
T: wgpu::rwh::HasDisplayHandle + Clone + core::fmt::Debug + Send + Sync + 'static,
|
||||
{
|
||||
fn clone_for_wgpu(&self) -> Box<dyn wgpu::wgt::WgpuHasDisplayHandle> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
|
||||
fn clone_display_handle(&self) -> Box<dyn EguiDisplayHandle> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[expect(clippy::large_enum_variant)]
|
||||
pub enum WgpuSetup {
|
||||
/// Construct a wgpu setup using some predefined settings & heuristics.
|
||||
/// This is the default option. You can customize most behaviours overriding the
|
||||
/// supported backends, power preferences, and device description.
|
||||
///
|
||||
/// By default can also be configured with various environment variables:
|
||||
/// * `WGPU_BACKEND`: `vulkan`, `dx12`, `metal`, `opengl`, `webgpu`
|
||||
/// * `WGPU_POWER_PREF`: `low`, `high` or `none`
|
||||
/// * `WGPU_TRACE`: Path to a file to output a wgpu trace file.
|
||||
///
|
||||
/// Each instance flag also comes with an environment variable (for details see [`wgpu::InstanceFlags`]):
|
||||
/// * `WGPU_VALIDATION`: Enables validation (enabled by default in debug builds).
|
||||
/// * `WGPU_DEBUG`: Generate debug information in shaders and objects (enabled by default in debug builds).
|
||||
/// * `WGPU_ALLOW_UNDERLYING_NONCOMPLIANT_ADAPTER`: Whether wgpu should expose adapters that run on top of non-compliant adapters.
|
||||
/// * `WGPU_GPU_BASED_VALIDATION`: Enable GPU-based validation.
|
||||
CreateNew(WgpuSetupCreateNew),
|
||||
|
||||
/// Run on an existing wgpu setup.
|
||||
Existing(WgpuSetupExisting),
|
||||
}
|
||||
|
||||
impl WgpuSetup {
|
||||
/// Creates a new [`WgpuSetup::CreateNew`] with the given display handle.
|
||||
///
|
||||
/// See [`WgpuSetupCreateNew::from_display_handle`] for details.
|
||||
pub fn from_display_handle(display_handle: impl EguiDisplayHandle) -> Self {
|
||||
Self::CreateNew(WgpuSetupCreateNew::from_display_handle(display_handle))
|
||||
}
|
||||
|
||||
/// Creates a new [`WgpuSetup::CreateNew`] without a display handle.
|
||||
///
|
||||
/// See [`WgpuSetupCreateNew::without_display_handle`] for details.
|
||||
pub fn without_display_handle() -> Self {
|
||||
Self::CreateNew(WgpuSetupCreateNew::without_display_handle())
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for WgpuSetup {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
Self::CreateNew(create_new) => f
|
||||
.debug_tuple("WgpuSetup::CreateNew")
|
||||
.field(create_new)
|
||||
.finish(),
|
||||
Self::Existing { .. } => f.debug_tuple("WgpuSetup::Existing").finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WgpuSetup {
|
||||
/// Creates a new [`wgpu::Instance`] or clones the existing one.
|
||||
///
|
||||
/// Does *not* store the wgpu instance, so calling this repeatedly may
|
||||
/// create a new instance every time!
|
||||
pub async fn new_instance(&self) -> wgpu::Instance {
|
||||
match self {
|
||||
Self::CreateNew(create_new) => {
|
||||
#[allow(clippy::allow_attributes, unused_mut)]
|
||||
let mut backends = create_new.instance_descriptor.backends;
|
||||
|
||||
// Don't try WebGPU if we're not in a secure context.
|
||||
// Emscripten is excluded: wgpu gates both its `webgpu` backend and
|
||||
// its `web_sys` re-export on `not(Emscripten)` (see the cfg aliases
|
||||
// in `wgpu/build.rs`).
|
||||
#[cfg(all(target_arch = "wasm32", not(target_os = "emscripten")))]
|
||||
if backends.contains(wgpu::Backends::BROWSER_WEBGPU) {
|
||||
let is_secure_context =
|
||||
wgpu::web_sys::window().is_some_and(|w| w.is_secure_context());
|
||||
if !is_secure_context {
|
||||
log::info!(
|
||||
"WebGPU is only available in secure contexts, i.e. on HTTPS and on localhost."
|
||||
);
|
||||
backends.remove(wgpu::Backends::BROWSER_WEBGPU);
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!("Creating wgpu instance with backends {backends:?}");
|
||||
let desc = &create_new.instance_descriptor;
|
||||
let descriptor = wgpu::InstanceDescriptor {
|
||||
backends: desc.backends,
|
||||
flags: desc.flags,
|
||||
backend_options: desc.backend_options.clone(),
|
||||
memory_budget_thresholds: desc.memory_budget_thresholds,
|
||||
display: create_new
|
||||
.display_handle
|
||||
.as_ref()
|
||||
.map(|handle| handle.clone_for_wgpu()),
|
||||
};
|
||||
wgpu::util::new_instance_with_webgpu_detection(descriptor).await
|
||||
}
|
||||
Self::Existing(existing) => existing.instance.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WgpuSetupCreateNew> for WgpuSetup {
|
||||
fn from(create_new: WgpuSetupCreateNew) -> Self {
|
||||
Self::CreateNew(create_new)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WgpuSetupExisting> for WgpuSetup {
|
||||
fn from(existing: WgpuSetupExisting) -> Self {
|
||||
Self::Existing(existing)
|
||||
}
|
||||
}
|
||||
|
||||
/// Method for selecting an adapter on native.
|
||||
///
|
||||
/// This can be used for fully custom adapter selection.
|
||||
/// If available, `wgpu::Surface` is passed to allow checking for surface compatibility.
|
||||
pub type NativeAdapterSelectorMethod = Arc<
|
||||
dyn Fn(&[wgpu::Adapter], Option<&wgpu::Surface<'_>>) -> Result<wgpu::Adapter, String>
|
||||
+ Send
|
||||
+ Sync,
|
||||
>;
|
||||
|
||||
/// Configuration for creating a new wgpu setup.
|
||||
///
|
||||
/// Used for [`WgpuSetup::CreateNew`].
|
||||
///
|
||||
/// Prefer [`Self::from_display_handle`] when you have a display handle available.
|
||||
/// Most platforms work without one, but some (e.g. Wayland with GLES, or WebGL)
|
||||
/// require it, so providing one ensures maximum compatibility.
|
||||
/// With winit, pass [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle).
|
||||
///
|
||||
/// Note: The display handle is stored in [`Self::display_handle`] rather than in
|
||||
/// [`Self::instance_descriptor`] so the config can be cloned
|
||||
/// ([`wgpu::InstanceDescriptor`] is not `Clone`). It is injected at instance creation time.
|
||||
pub struct WgpuSetupCreateNew {
|
||||
/// Descriptor for the wgpu instance.
|
||||
///
|
||||
/// Leave [`wgpu::InstanceDescriptor::display`] as `None` — use [`Self::display_handle`]
|
||||
/// instead (injected at instance creation time).
|
||||
///
|
||||
/// The most important field is [`wgpu::InstanceDescriptor::backends`], which controls
|
||||
/// which backends are supported (wgpu will pick one of these). For example, set it to
|
||||
/// [`wgpu::Backends::GL`] to use only WebGL. By default on web, WebGPU is preferred
|
||||
/// with WebGL as a fallback (requires the `webgl` feature of crate `wgpu`).
|
||||
pub instance_descriptor: wgpu::InstanceDescriptor,
|
||||
|
||||
/// Display handle passed to wgpu at instance creation time.
|
||||
///
|
||||
/// Required on some platforms (e.g. Wayland with GLES, WebGL); optional elsewhere.
|
||||
/// With winit, use [`winit::event_loop::OwnedDisplayHandle`].
|
||||
///
|
||||
/// `eframe` 's winit & web integrations will attempt to fill the display handle automatically if it is left empty.
|
||||
pub display_handle: Option<Box<dyn EguiDisplayHandle>>,
|
||||
|
||||
/// Power preference for the adapter if [`Self::native_adapter_selector`] is not set or targeting web.
|
||||
pub power_preference: wgpu::PowerPreference,
|
||||
|
||||
/// Optional selector for native adapters.
|
||||
///
|
||||
/// This field has no effect when targeting web!
|
||||
/// Otherwise, if set [`Self::power_preference`] is ignored and the adapter is instead selected by this method.
|
||||
/// Note that [`Self::instance_descriptor`]'s [`wgpu::InstanceDescriptor::backends`]
|
||||
/// are still used to filter the adapter enumeration in the first place.
|
||||
///
|
||||
/// Defaults to `None`.
|
||||
pub native_adapter_selector: Option<NativeAdapterSelectorMethod>,
|
||||
|
||||
/// Configuration passed on device request, given an adapter
|
||||
pub device_descriptor:
|
||||
Arc<dyn Fn(&wgpu::Adapter) -> wgpu::DeviceDescriptor<'static> + Send + Sync>,
|
||||
}
|
||||
|
||||
impl WgpuSetupCreateNew {
|
||||
/// Creates a new configuration with the given display handle.
|
||||
///
|
||||
/// This is the recommended constructor. Most platforms (Windows, macOS/iOS, Android, web)
|
||||
/// work fine without a display handle, but some (e.g. Wayland on Linux with GLES) require
|
||||
/// one. Providing it unconditionally ensures your app works everywhere.
|
||||
///
|
||||
/// If you don't have a display handle available, use [`Self::without_display_handle`]
|
||||
/// instead — it will still work on the majority of platforms.
|
||||
///
|
||||
/// With winit, pass [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle).
|
||||
pub fn from_display_handle(display_handle: impl EguiDisplayHandle) -> Self {
|
||||
Self {
|
||||
display_handle: Some(Box::new(display_handle)),
|
||||
..Self::without_display_handle()
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new configuration without a display handle.
|
||||
///
|
||||
/// A display handle is not required for headless operation (offscreen rendering, tests,
|
||||
/// compute-only workloads). It also isn't needed on most platforms even when presenting
|
||||
/// to a window — only some configurations (e.g. Wayland on Linux with GLES) require one.
|
||||
///
|
||||
/// If you do have a display handle available, prefer [`Self::from_display_handle`] for
|
||||
/// maximum compatibility.
|
||||
///
|
||||
/// With winit you can obtain one via [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle).
|
||||
///
|
||||
/// `eframe` 's winit & web integrations will attempt to fill the display handle automatically if it is left empty.
|
||||
pub fn without_display_handle() -> Self {
|
||||
Self {
|
||||
instance_descriptor: wgpu::InstanceDescriptor {
|
||||
// Add GL backend, primarily because WebGPU is not stable enough yet.
|
||||
// (note however, that the GL backend needs to be opted-in via the wgpu feature flag "webgl")
|
||||
backends: wgpu::Backends::from_env()
|
||||
.unwrap_or(wgpu::Backends::PRIMARY | wgpu::Backends::GL),
|
||||
flags: wgpu::InstanceFlags::from_build_config().with_env(),
|
||||
backend_options: wgpu::BackendOptions::from_env_or_default(),
|
||||
memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
|
||||
display: None,
|
||||
},
|
||||
|
||||
display_handle: None,
|
||||
|
||||
power_preference: wgpu::PowerPreference::from_env()
|
||||
.unwrap_or(wgpu::PowerPreference::HighPerformance),
|
||||
|
||||
native_adapter_selector: None,
|
||||
|
||||
device_descriptor: Arc::new(|adapter| {
|
||||
let base_limits = if adapter.get_info().backend == wgpu::Backend::Gl {
|
||||
wgpu::Limits::downlevel_webgl2_defaults()
|
||||
} else {
|
||||
wgpu::Limits::default()
|
||||
};
|
||||
|
||||
wgpu::DeviceDescriptor {
|
||||
label: Some("egui wgpu device"),
|
||||
required_limits: wgpu::Limits {
|
||||
// When using a depth buffer, we have to be able to create a texture
|
||||
// large enough for the entire surface, and we want to support 4k+ displays.
|
||||
max_texture_dimension_2d: 8192,
|
||||
..base_limits
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for WgpuSetupCreateNew {
|
||||
fn clone(&self) -> Self {
|
||||
let desc = &self.instance_descriptor;
|
||||
Self {
|
||||
instance_descriptor: wgpu::InstanceDescriptor {
|
||||
backends: desc.backends,
|
||||
flags: desc.flags,
|
||||
backend_options: desc.backend_options.clone(),
|
||||
memory_budget_thresholds: desc.memory_budget_thresholds,
|
||||
display: None,
|
||||
},
|
||||
display_handle: self.display_handle.clone(),
|
||||
power_preference: self.power_preference,
|
||||
native_adapter_selector: self.native_adapter_selector.clone(),
|
||||
device_descriptor: Arc::clone(&self.device_descriptor),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for WgpuSetupCreateNew {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
let Self {
|
||||
instance_descriptor,
|
||||
display_handle,
|
||||
power_preference,
|
||||
native_adapter_selector,
|
||||
device_descriptor: _,
|
||||
} = self;
|
||||
f.debug_struct("WgpuSetupCreateNew")
|
||||
.field("instance_descriptor", instance_descriptor)
|
||||
.field("display_handle", display_handle)
|
||||
.field("power_preference", power_preference)
|
||||
.field(
|
||||
"native_adapter_selector",
|
||||
&native_adapter_selector.is_some(),
|
||||
)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for using an existing wgpu setup.
|
||||
///
|
||||
/// Used for [`WgpuSetup::Existing`].
|
||||
#[derive(Clone)]
|
||||
pub struct WgpuSetupExisting {
|
||||
pub instance: wgpu::Instance,
|
||||
pub adapter: wgpu::Adapter,
|
||||
pub device: wgpu::Device,
|
||||
pub queue: wgpu::Queue,
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
struct VertexOutput {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
};
|
||||
|
||||
var<private> positions: array<vec2f, 3> = array<vec2f, 3>(
|
||||
vec2f(-1.0, -3.0),
|
||||
vec2f(-1.0, 1.0),
|
||||
vec2f(3.0, 1.0)
|
||||
);
|
||||
|
||||
// meant to be called with 3 vertex indices: 0, 1, 2
|
||||
// draws one large triangle over the clip space like this:
|
||||
// (the asterisks represent the clip space bounds)
|
||||
//-1,1 1,1
|
||||
// ---------------------------------
|
||||
// | * .
|
||||
// | * .
|
||||
// | * .
|
||||
// | * .
|
||||
// | * .
|
||||
// | * .
|
||||
// |***************
|
||||
// | . 1,-1
|
||||
// | .
|
||||
// | .
|
||||
// | .
|
||||
// | .
|
||||
// |.
|
||||
@vertex
|
||||
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
|
||||
var result: VertexOutput;
|
||||
result.position = vec4f(positions[vertex_index], 0.0, 1.0);
|
||||
return result;
|
||||
}
|
||||
|
||||
@group(0)
|
||||
@binding(0)
|
||||
var r_color: texture_2d<f32>;
|
||||
|
||||
@fragment
|
||||
fn fs_main(vertex: VertexOutput) -> @location(0) vec4<f32> {
|
||||
return textureLoad(r_color, vec2i(vertex.position.xy), 0);
|
||||
}
|
||||
Vendored
+869
@@ -0,0 +1,869 @@
|
||||
#![expect(clippy::missing_errors_doc)]
|
||||
#![expect(clippy::undocumented_unsafe_blocks)]
|
||||
#![expect(clippy::unwrap_used)] // TODO(emilk): avoid unwraps
|
||||
#![expect(unsafe_code)]
|
||||
|
||||
use crate::{RenderState, SurfaceConfig, SurfaceErrorAction, WgpuConfiguration, renderer};
|
||||
use crate::{
|
||||
RendererOptions,
|
||||
capture::{CaptureReceiver, CaptureSender, CaptureState, capture_channel},
|
||||
};
|
||||
use core::num::NonZeroU32;
|
||||
use egui::{Context, Event, UserData, ViewportId, ViewportIdMap, ViewportIdSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
struct SurfaceState {
|
||||
surface: wgpu::Surface<'static>,
|
||||
alpha_mode: wgpu::CompositeAlphaMode,
|
||||
width: u32,
|
||||
height: u32,
|
||||
// PdfCraft patch (#577): `width` and `height` are the size the surface is configured at,
|
||||
// which `surface_fit` keeps within the device's `max_texture_dimension_2d`; these are the
|
||||
// window's own size, and `render_scale` is how much smaller the surface is drawn.
|
||||
window_width: u32,
|
||||
window_height: u32,
|
||||
render_scale: f32,
|
||||
resizing: bool,
|
||||
needs_reconfigure: bool,
|
||||
needs_recreate: bool,
|
||||
}
|
||||
|
||||
/// PdfCraft patch (#577): the size to configure a window's surface at, and the factor egui
|
||||
/// draws it at, so that neither side exceeds `max_side` (the device's
|
||||
/// `max_texture_dimension_2d`).
|
||||
///
|
||||
/// `Surface::configure` rejects a larger surface, and wgpu reports that as an uncaptured
|
||||
/// validation error, which panics: a window bigger than the GPU's limit (restored at another
|
||||
/// monitor's scale, stretched across monitors, …) closed the app before it opened. Both sides
|
||||
/// shrink by the same factor and egui draws at that factor, so the whole window is drawn into
|
||||
/// the surface. Where the swapchain is stretched over the window (DX12 uses
|
||||
/// `DXGI_SCALING_STRETCH`) it fills the window and lines up with the pointer, only softer;
|
||||
/// wgpu's GL backend copies it unscaled into a corner instead. Degraded either way, but not a
|
||||
/// crash. A window within the limit is returned unchanged, with a factor of 1. The longer side
|
||||
/// becomes exactly `max_side`, and a side that isn't zero never becomes zero (an empty surface
|
||||
/// can't be configured either).
|
||||
pub fn surface_fit(width: u32, height: u32, max_side: u32) -> (u32, u32, f32) {
|
||||
let max_side = max_side.max(1);
|
||||
if width <= max_side && height <= max_side {
|
||||
return (width, height, 1.0);
|
||||
}
|
||||
let longest = width.max(height);
|
||||
let scale = (f64::from(max_side) / f64::from(longest)).min(1.0);
|
||||
// `as` saturates and `scale` is finite and in (0, 1]: no overflow, NaN or panic. The longer
|
||||
// side is set rather than computed, so floating-point rounding can't leave it a pixel short.
|
||||
let fit = |side: u32| {
|
||||
if side == 0 {
|
||||
0
|
||||
} else if side == longest {
|
||||
max_side
|
||||
} else {
|
||||
((f64::from(side) * scale).floor() as u32).clamp(1, max_side)
|
||||
}
|
||||
};
|
||||
(fit(width), fit(height), scale as f32)
|
||||
}
|
||||
|
||||
/// Everything you need to paint egui with [`wgpu`] on [`winit`].
|
||||
///
|
||||
/// Alternatively you can use [`crate::Renderer`] directly.
|
||||
///
|
||||
/// NOTE: all egui viewports share the same painter.
|
||||
pub struct Painter {
|
||||
context: Context,
|
||||
config: WgpuConfiguration,
|
||||
options: RendererOptions,
|
||||
support_transparent_backbuffer: bool,
|
||||
screen_capture_state: Option<CaptureState>,
|
||||
|
||||
instance: wgpu::Instance,
|
||||
render_state: Option<RenderState>,
|
||||
|
||||
// Per viewport/window:
|
||||
depth_texture_view: ViewportIdMap<wgpu::TextureView>,
|
||||
msaa_texture_view: ViewportIdMap<wgpu::TextureView>,
|
||||
surfaces: ViewportIdMap<SurfaceState>,
|
||||
capture_tx: CaptureSender,
|
||||
capture_rx: CaptureReceiver,
|
||||
}
|
||||
|
||||
impl Painter {
|
||||
/// Manages [`wgpu`] state, including surface state, required to render egui.
|
||||
///
|
||||
/// Only the [`wgpu::Instance`] is initialized here. Device selection and the initialization
|
||||
/// of render + surface state is deferred until the painter is given its first window target
|
||||
/// via [`set_window()`](Self::set_window). (Ensuring that a device that's compatible with the
|
||||
/// native window is chosen)
|
||||
///
|
||||
/// Before calling [`paint_and_update_textures()`](Self::paint_and_update_textures) a
|
||||
/// [`wgpu::Surface`] must be initialized (and corresponding render state) by calling
|
||||
/// [`set_window()`](Self::set_window) once you have
|
||||
/// a [`winit::window::Window`] with a valid `.raw_window_handle()`
|
||||
/// associated.
|
||||
pub async fn new(
|
||||
context: Context,
|
||||
config: WgpuConfiguration,
|
||||
support_transparent_backbuffer: bool,
|
||||
options: RendererOptions,
|
||||
) -> Self {
|
||||
let (capture_tx, capture_rx) = capture_channel();
|
||||
let instance = config.wgpu_setup.new_instance().await;
|
||||
|
||||
Self {
|
||||
context,
|
||||
config,
|
||||
options,
|
||||
support_transparent_backbuffer,
|
||||
screen_capture_state: None,
|
||||
|
||||
instance,
|
||||
render_state: None,
|
||||
|
||||
depth_texture_view: Default::default(),
|
||||
surfaces: Default::default(),
|
||||
msaa_texture_view: Default::default(),
|
||||
|
||||
capture_tx,
|
||||
capture_rx,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the [`RenderState`].
|
||||
///
|
||||
/// Will return [`None`] if the render state has not been initialized yet.
|
||||
pub fn render_state(&self) -> Option<RenderState> {
|
||||
self.render_state.clone()
|
||||
}
|
||||
|
||||
fn configure_surface(
|
||||
surface_state: &SurfaceState,
|
||||
render_state: &RenderState,
|
||||
config: &SurfaceConfig,
|
||||
) {
|
||||
profiling::function_scope!();
|
||||
|
||||
let SurfaceConfig {
|
||||
present_mode,
|
||||
desired_maximum_frame_latency,
|
||||
} = *config;
|
||||
|
||||
// Transaction presentation can hold a drawable during AppKit live resize. Keep the
|
||||
// configured low-latency path normally, but use three Metal drawables while resizing.
|
||||
#[cfg(all(target_os = "macos", feature = "macos-window-resize-jitter-fix"))]
|
||||
let desired_maximum_frame_latency = if surface_state.resizing {
|
||||
Some(desired_maximum_frame_latency.unwrap_or(2).max(2))
|
||||
} else {
|
||||
desired_maximum_frame_latency
|
||||
};
|
||||
|
||||
let width = surface_state.width;
|
||||
let height = surface_state.height;
|
||||
|
||||
let mut surf_config = wgpu::SurfaceConfiguration {
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
format: render_state.target_format,
|
||||
present_mode,
|
||||
alpha_mode: surface_state.alpha_mode,
|
||||
view_formats: vec![render_state.target_format],
|
||||
..surface_state
|
||||
.surface
|
||||
.get_default_config(&render_state.adapter, width, height)
|
||||
.expect("The surface isn't supported by this adapter")
|
||||
};
|
||||
|
||||
if let Some(desired_maximum_frame_latency) = desired_maximum_frame_latency {
|
||||
surf_config.desired_maximum_frame_latency = desired_maximum_frame_latency;
|
||||
}
|
||||
|
||||
surface_state
|
||||
.surface
|
||||
.configure(&render_state.device, &surf_config);
|
||||
}
|
||||
|
||||
/// Drop the existing [`wgpu::Surface`] for `viewport_id` and create a fresh one for the
|
||||
/// given window via [`wgpu::Instance::create_surface`], then configure it.
|
||||
///
|
||||
/// Used to recover from [`wgpu::CurrentSurfaceTexture::Lost`], where reconfiguring the
|
||||
/// existing surface object cannot recover.
|
||||
fn recreate_surface(
|
||||
&mut self,
|
||||
viewport_id: ViewportId,
|
||||
window: &Arc<winit::window::Window>,
|
||||
) -> Result<(), crate::WgpuError> {
|
||||
profiling::function_scope!();
|
||||
|
||||
let Some(old_state) = self.surfaces.remove(&viewport_id) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let surface = self.instance.create_surface(Arc::clone(window))?;
|
||||
// PdfCraft patch (#577): the window's size; `width` and `height` may have been fitted.
|
||||
self.install_surface(
|
||||
surface,
|
||||
viewport_id,
|
||||
old_state.window_width,
|
||||
old_state.window_height,
|
||||
old_state.resizing,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates (or clears) the [`winit::window::Window`] associated with the [`Painter`]
|
||||
///
|
||||
/// This creates a [`wgpu::Surface`] for the given Window (as well as initializing render
|
||||
/// state if needed) that is used for egui rendering.
|
||||
///
|
||||
/// This must be called before trying to render via
|
||||
/// [`paint_and_update_textures`](Self::paint_and_update_textures)
|
||||
///
|
||||
/// # Portability
|
||||
///
|
||||
/// _In particular it's important to note that on Android a it's only possible to create
|
||||
/// a window surface between `Resumed` and `Paused` lifecycle events, and Winit will panic on
|
||||
/// attempts to query the raw window handle while paused._
|
||||
///
|
||||
/// On Android [`set_window`](Self::set_window) should be called with `Some(window)` for each
|
||||
/// `Resumed` event and `None` for each `Paused` event. Currently, on all other platforms
|
||||
/// [`set_window`](Self::set_window) may be called with `Some(window)` as soon as you have a
|
||||
/// valid [`winit::window::Window`].
|
||||
///
|
||||
/// # Errors
|
||||
/// If the provided wgpu configuration does not match an available device.
|
||||
pub async fn set_window(
|
||||
&mut self,
|
||||
viewport_id: ViewportId,
|
||||
window: Option<Arc<winit::window::Window>>,
|
||||
) -> Result<(), crate::WgpuError> {
|
||||
profiling::scope!("Painter::set_window"); // profile_function gives bad names for async functions
|
||||
|
||||
if let Some(window) = window {
|
||||
let size = window.inner_size();
|
||||
if !self.surfaces.contains_key(&viewport_id) {
|
||||
let surface = self.instance.create_surface(window)?;
|
||||
self.add_surface(surface, viewport_id, size).await?;
|
||||
}
|
||||
} else {
|
||||
log::warn!("No window - clearing all surfaces");
|
||||
self.surfaces.clear();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates (or clears) the [`winit::window::Window`] associated with the [`Painter`] without taking ownership of the window.
|
||||
///
|
||||
/// Like [`set_window`](Self::set_window) except:
|
||||
///
|
||||
/// # Safety
|
||||
/// The user is responsible for ensuring that the window is alive for as long as it is set.
|
||||
pub async unsafe fn set_window_unsafe(
|
||||
&mut self,
|
||||
viewport_id: ViewportId,
|
||||
window: Option<&winit::window::Window>,
|
||||
) -> Result<(), crate::WgpuError> {
|
||||
profiling::scope!("Painter::set_window_unsafe"); // profile_function gives bad names for async functions
|
||||
|
||||
if let Some(window) = window {
|
||||
let size = window.inner_size();
|
||||
if !self.surfaces.contains_key(&viewport_id) {
|
||||
let surface = unsafe {
|
||||
self.instance
|
||||
.create_surface_unsafe(wgpu::SurfaceTargetUnsafe::from_window(&window)?)?
|
||||
};
|
||||
self.add_surface(surface, viewport_id, size).await?;
|
||||
}
|
||||
} else {
|
||||
log::warn!("No window - clearing all surfaces");
|
||||
self.surfaces.clear();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_surface(
|
||||
&mut self,
|
||||
surface: wgpu::Surface<'static>,
|
||||
viewport_id: ViewportId,
|
||||
size: winit::dpi::PhysicalSize<u32>,
|
||||
) -> Result<(), crate::WgpuError> {
|
||||
if self.render_state.is_none() {
|
||||
let render_state =
|
||||
RenderState::create(&self.config, &self.instance, Some(&surface), self.options)
|
||||
.await?;
|
||||
self.render_state = Some(render_state);
|
||||
}
|
||||
self.install_surface(surface, viewport_id, size.width, size.height, false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Inserts a freshly created surface into [`Self::surfaces`] and configures it.
|
||||
///
|
||||
/// Render state must already be initialised before calling this.
|
||||
// NOTE: The same assumption is already required by `resize_and_generate_depth_texture_view_and_msaa_view`.
|
||||
fn install_surface(
|
||||
&mut self,
|
||||
surface: wgpu::Surface<'static>,
|
||||
viewport_id: ViewportId,
|
||||
width: u32,
|
||||
height: u32,
|
||||
resizing: bool,
|
||||
) {
|
||||
let alpha_mode = {
|
||||
// Panic: We use the same failure mode as `resize_and_generate_depth_texture_view_and_msaa_view`
|
||||
let render_state = self
|
||||
.render_state
|
||||
.as_ref()
|
||||
.expect("install_surface called before render_state initialization");
|
||||
if self.support_transparent_backbuffer {
|
||||
let supported_alpha_modes =
|
||||
surface.get_capabilities(&render_state.adapter).alpha_modes;
|
||||
// Prefer pre multiplied over post multiplied!
|
||||
if supported_alpha_modes.contains(&wgpu::CompositeAlphaMode::PreMultiplied) {
|
||||
wgpu::CompositeAlphaMode::PreMultiplied
|
||||
} else if supported_alpha_modes.contains(&wgpu::CompositeAlphaMode::PostMultiplied)
|
||||
{
|
||||
wgpu::CompositeAlphaMode::PostMultiplied
|
||||
} else {
|
||||
log::warn!(
|
||||
"Transparent window was requested, but the active wgpu surface does not support a `CompositeAlphaMode` with transparency."
|
||||
);
|
||||
wgpu::CompositeAlphaMode::Auto
|
||||
}
|
||||
} else {
|
||||
wgpu::CompositeAlphaMode::Auto
|
||||
}
|
||||
};
|
||||
// PdfCraft patch (#577): store the fitted size, so nothing configures the window's own.
|
||||
// `render_scale` starts at 1 so that the resize below, which sets it, logs the fit once.
|
||||
let (fit_width, fit_height, _) = surface_fit(width, height, self.max_surface_side());
|
||||
self.surfaces.insert(
|
||||
viewport_id,
|
||||
SurfaceState {
|
||||
surface,
|
||||
width: fit_width,
|
||||
height: fit_height,
|
||||
window_width: width,
|
||||
window_height: height,
|
||||
render_scale: 1.0,
|
||||
alpha_mode,
|
||||
resizing,
|
||||
needs_reconfigure: false,
|
||||
needs_recreate: false,
|
||||
},
|
||||
);
|
||||
let Some(width) = NonZeroU32::new(width) else {
|
||||
log::debug!("The window width was zero; skipping generate textures");
|
||||
return;
|
||||
};
|
||||
let Some(height) = NonZeroU32::new(height) else {
|
||||
log::debug!("The window height was zero; skipping generate textures");
|
||||
return;
|
||||
};
|
||||
self.resize_and_generate_depth_texture_view_and_msaa_view(viewport_id, width, height);
|
||||
}
|
||||
|
||||
/// Returns the maximum texture dimension supported if known
|
||||
///
|
||||
/// This API will only return a known dimension after `set_window()` has been called
|
||||
/// at least once, since the underlying device and render state are initialized lazily
|
||||
/// once we have a window (that may determine the choice of adapter/device).
|
||||
pub fn max_texture_side(&self) -> Option<usize> {
|
||||
self.render_state
|
||||
.as_ref()
|
||||
.map(|rs| rs.device.limits().max_texture_dimension_2d as usize)
|
||||
}
|
||||
|
||||
/// PdfCraft patch (#577): the largest side a surface may be configured at, or no limit
|
||||
/// before the device exists (nothing is configured then).
|
||||
fn max_surface_side(&self) -> u32 {
|
||||
self.render_state
|
||||
.as_ref()
|
||||
.map_or(u32::MAX, |rs| rs.device.limits().max_texture_dimension_2d)
|
||||
}
|
||||
|
||||
fn resize_and_generate_depth_texture_view_and_msaa_view(
|
||||
&mut self,
|
||||
viewport_id: ViewportId,
|
||||
width_in_pixels: NonZeroU32,
|
||||
height_in_pixels: NonZeroU32,
|
||||
) {
|
||||
profiling::function_scope!();
|
||||
|
||||
// PdfCraft patch (#577): configure the surface, and size the depth and MSAA textures,
|
||||
// within the device's limit; `surface_fit` keeps a non-zero side non-zero.
|
||||
let max_side = self.max_surface_side();
|
||||
let (window_width, window_height) = (width_in_pixels.get(), height_in_pixels.get());
|
||||
let (width, height, render_scale) = surface_fit(window_width, window_height, max_side);
|
||||
|
||||
let render_state = self.render_state.as_ref().unwrap();
|
||||
let surface_state = self.surfaces.get_mut(&viewport_id).unwrap();
|
||||
|
||||
if render_scale < 1.0 && surface_state.render_scale >= 1.0 {
|
||||
log::warn!(
|
||||
"the window is {window_width} × {window_height} pixels, more than this GPU draws \
|
||||
({max_side} a side): drawing it at {width} × {height} and scaling it up"
|
||||
);
|
||||
}
|
||||
surface_state.width = width;
|
||||
surface_state.height = height;
|
||||
surface_state.window_width = window_width;
|
||||
surface_state.window_height = window_height;
|
||||
surface_state.render_scale = render_scale;
|
||||
|
||||
Self::configure_surface(surface_state, render_state, &self.config.surface);
|
||||
|
||||
if let Some(depth_format) = self.options.depth_stencil_format {
|
||||
self.depth_texture_view.insert(
|
||||
viewport_id,
|
||||
render_state
|
||||
.device
|
||||
.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("egui_depth_texture"),
|
||||
size: wgpu::Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: self.options.msaa_samples.max(1),
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: depth_format,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
|
||||
| wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[depth_format],
|
||||
})
|
||||
.create_view(&wgpu::TextureViewDescriptor::default()),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(render_state) = (self.options.msaa_samples > 1)
|
||||
.then_some(self.render_state.as_ref())
|
||||
.flatten()
|
||||
{
|
||||
let texture_format = render_state.target_format;
|
||||
self.msaa_texture_view.insert(
|
||||
viewport_id,
|
||||
render_state
|
||||
.device
|
||||
.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("egui_msaa_texture"),
|
||||
size: wgpu::Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: self.options.msaa_samples.max(1),
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: texture_format,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
view_formats: &[texture_format],
|
||||
})
|
||||
.create_view(&wgpu::TextureViewDescriptor::default()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles changes of the resizing state.
|
||||
///
|
||||
/// Should be called prior to the first [`Painter::on_window_resized`] call and after the last in
|
||||
/// the chain. Used to apply platform-specific logic, e.g. OSX Metal window resize jitter fix.
|
||||
pub fn on_window_resize_state_change(&mut self, viewport_id: ViewportId, resizing: bool) {
|
||||
profiling::function_scope!();
|
||||
|
||||
let Some(state) = self.surfaces.get_mut(&viewport_id) else {
|
||||
return;
|
||||
};
|
||||
if state.resizing == resizing {
|
||||
if resizing {
|
||||
log::debug!(
|
||||
"Painter::on_window_resize_state_change() redundant call while resizing"
|
||||
);
|
||||
} else {
|
||||
log::debug!(
|
||||
"Painter::on_window_resize_state_change() redundant call after resizing"
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Set before reconfiguring so macOS live resize uses the temporary latency bump above.
|
||||
state.resizing = resizing;
|
||||
|
||||
// Resizing is a bit tricky on macOS.
|
||||
// It requires enabling ["present_with_transaction"](https://developer.apple.com/documentation/quartzcore/cametallayer/presentswithtransaction)
|
||||
// flag to avoid jittering during the resize. Even though resize jittering on macOS
|
||||
// is common across rendering backends, the solution for wgpu/metal is known.
|
||||
//
|
||||
// See https://github.com/emilk/egui/issues/903
|
||||
#[cfg(all(target_os = "macos", feature = "macos-window-resize-jitter-fix"))]
|
||||
{
|
||||
// SAFETY: `as_hal::<Metal>()` returns `None` unless this surface is backed by wgpu's
|
||||
// Metal backend.
|
||||
unsafe {
|
||||
if let (Some(render_state), Some(hal_surface)) = (
|
||||
self.render_state.as_ref(),
|
||||
state.surface.as_hal::<wgpu::hal::api::Metal>(),
|
||||
) {
|
||||
hal_surface
|
||||
.render_layer()
|
||||
.lock()
|
||||
.setPresentsWithTransaction(resizing);
|
||||
|
||||
Self::configure_surface(state, render_state, &self.config.surface);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_window_resized(
|
||||
&mut self,
|
||||
viewport_id: ViewportId,
|
||||
width_in_pixels: NonZeroU32,
|
||||
height_in_pixels: NonZeroU32,
|
||||
) {
|
||||
profiling::function_scope!();
|
||||
|
||||
if self.surfaces.contains_key(&viewport_id) {
|
||||
self.resize_and_generate_depth_texture_view_and_msaa_view(
|
||||
viewport_id,
|
||||
width_in_pixels,
|
||||
height_in_pixels,
|
||||
);
|
||||
} else {
|
||||
log::warn!(
|
||||
"Ignoring window resize notification with no surface created via Painter::set_window()"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns two things:
|
||||
///
|
||||
/// The approximate number of seconds spent on vsync-waiting (if any),
|
||||
/// and the captures captured screenshot if it was requested.
|
||||
///
|
||||
/// If `capture_data` isn't empty, a screenshot will be captured.
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
pub fn paint_and_update_textures(
|
||||
&mut self,
|
||||
viewport_id: ViewportId,
|
||||
pixels_per_point: f32,
|
||||
clear_color: [f32; 4],
|
||||
clipped_primitives: &[epaint::ClippedPrimitive],
|
||||
textures_delta: &mut epaint::textures::TexturesDelta,
|
||||
capture_data: Vec<UserData>,
|
||||
window: &Arc<winit::window::Window>,
|
||||
) -> f32 {
|
||||
profiling::function_scope!();
|
||||
|
||||
/// Guard to ensure that commands are always submitted to the renderer queue
|
||||
/// so that calls to [`write_buffer()`](https://docs.rs/wgpu/latest/wgpu/struct.Queue.html#method.write_buffer)
|
||||
/// are completed even if we take a codepath which doesn't submit commands and avoids
|
||||
/// internal buffers growing indefinitely.
|
||||
///
|
||||
/// This may happen, for example, if no output frame is resolved.
|
||||
/// See <https://github.com/emilk/egui/pull/7928> for full context.
|
||||
struct RendererQueueGuard<'q> {
|
||||
queue: &'q wgpu::Queue,
|
||||
commands_submitted: bool,
|
||||
}
|
||||
|
||||
impl Drop for RendererQueueGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
// Only submit an empty command buffer array if no commands were
|
||||
// explicitly submitted.
|
||||
if !self.commands_submitted {
|
||||
self.queue.submit([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let capture = !capture_data.is_empty();
|
||||
let mut vsync_sec = 0.0;
|
||||
|
||||
// If the previous frame produced `CurrentSurfaceTexture::Lost`, the action match
|
||||
// below set `needs_recreate`. Recreate the surface now, before re-borrowing
|
||||
// `self.render_state` / `self.surfaces` for the rest of the paint.
|
||||
if self
|
||||
.surfaces
|
||||
.get(&viewport_id)
|
||||
.is_some_and(|s| s.needs_recreate)
|
||||
&& let Err(err) = self.recreate_surface(viewport_id, window)
|
||||
{
|
||||
log::error!("Failed to recreate surface for {viewport_id:?}: {err}");
|
||||
return vsync_sec;
|
||||
}
|
||||
|
||||
// Apply any runtime changes requested via `RenderState::surface_config`.
|
||||
// We diff against the already-applied values in `self.config.surface`
|
||||
// and, if anything differs, mark every surface as needing reconfiguration so
|
||||
// the existing `needs_reconfigure` pathway below picks them up.
|
||||
if let Some(render_state) = self.render_state.as_ref()
|
||||
&& render_state.surface_config != self.config.surface
|
||||
{
|
||||
self.config.surface = render_state.surface_config;
|
||||
#[expect(clippy::iter_over_hash_type)]
|
||||
for surface in self.surfaces.values_mut() {
|
||||
surface.needs_reconfigure = true;
|
||||
}
|
||||
}
|
||||
|
||||
let Some(render_state) = self.render_state.as_mut() else {
|
||||
return vsync_sec;
|
||||
};
|
||||
|
||||
let mut render_queue_guard = RendererQueueGuard {
|
||||
queue: &render_state.queue,
|
||||
commands_submitted: false,
|
||||
};
|
||||
|
||||
let Some(surface_state) = self.surfaces.get_mut(&viewport_id) else {
|
||||
return vsync_sec;
|
||||
};
|
||||
|
||||
let mut encoder =
|
||||
render_state
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("encoder"),
|
||||
});
|
||||
|
||||
// Upload all resources for the GPU.
|
||||
// PdfCraft patch (#577): a surface fitted within the GPU's limit is drawn smaller.
|
||||
let screen_descriptor = renderer::ScreenDescriptor {
|
||||
size_in_pixels: [surface_state.width, surface_state.height],
|
||||
pixels_per_point: pixels_per_point * surface_state.render_scale,
|
||||
};
|
||||
|
||||
let user_cmd_bufs = {
|
||||
let mut renderer = render_state.renderer.write();
|
||||
#[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
|
||||
for (id, image_deltas) in textures_delta.set.drain() {
|
||||
for image_delta in image_deltas {
|
||||
renderer.update_texture(
|
||||
&render_state.device,
|
||||
&render_state.queue,
|
||||
id,
|
||||
&image_delta,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
renderer.update_buffers(
|
||||
&render_state.device,
|
||||
&render_state.queue,
|
||||
&mut encoder,
|
||||
clipped_primitives,
|
||||
&screen_descriptor,
|
||||
)
|
||||
};
|
||||
|
||||
if surface_state.needs_reconfigure {
|
||||
Self::configure_surface(surface_state, render_state, &self.config.surface);
|
||||
surface_state.needs_reconfigure = false;
|
||||
}
|
||||
|
||||
let output_frame = {
|
||||
profiling::scope!("get_current_texture");
|
||||
// This is what vsync-waiting happens on my Mac.
|
||||
let start = web_time::Instant::now();
|
||||
let output_frame = surface_state.surface.get_current_texture();
|
||||
vsync_sec += start.elapsed().as_secs_f32();
|
||||
output_frame
|
||||
};
|
||||
|
||||
let output_frame = match output_frame {
|
||||
wgpu::CurrentSurfaceTexture::Success(frame) => frame,
|
||||
wgpu::CurrentSurfaceTexture::Suboptimal(frame) => {
|
||||
surface_state.needs_reconfigure = true;
|
||||
frame
|
||||
}
|
||||
other => {
|
||||
match (*self.config.on_surface_status)(&other) {
|
||||
SurfaceErrorAction::Reconfigure => {
|
||||
Self::configure_surface(surface_state, render_state, &self.config.surface);
|
||||
self.context.request_repaint_of(viewport_id);
|
||||
}
|
||||
SurfaceErrorAction::RecreateSurface => {
|
||||
// Because of ownership, I could not find an easy way to do a full recovery here,
|
||||
// as that would involve dropping the old surface and creating a new one.
|
||||
// For now, we defer the recreation to the beginning of the next frame (which
|
||||
// we ensure to arrive via `request_repaint_of`). A cleaner solution would be
|
||||
// to untangle the ownership of `RenderState`.
|
||||
surface_state.needs_recreate = true;
|
||||
self.context.request_repaint_of(viewport_id);
|
||||
}
|
||||
SurfaceErrorAction::SkipFrame => {}
|
||||
}
|
||||
return vsync_sec;
|
||||
}
|
||||
};
|
||||
|
||||
let mut capture_buffer = None;
|
||||
{
|
||||
let renderer = render_state.renderer.read();
|
||||
|
||||
let target_texture = if capture {
|
||||
let capture_state = self.screen_capture_state.get_or_insert_with(|| {
|
||||
CaptureState::new(&render_state.device, &output_frame.texture)
|
||||
});
|
||||
capture_state.update(&render_state.device, &output_frame.texture);
|
||||
|
||||
&capture_state.texture
|
||||
} else {
|
||||
&output_frame.texture
|
||||
};
|
||||
let target_view = target_texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
let (view, resolve_target) = (self.options.msaa_samples > 1)
|
||||
.then_some(self.msaa_texture_view.get(&viewport_id))
|
||||
.flatten()
|
||||
.map_or((&target_view, None), |texture_view| {
|
||||
(texture_view, Some(&target_view))
|
||||
});
|
||||
|
||||
let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("egui_render"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view,
|
||||
resolve_target,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color {
|
||||
r: clear_color[0] as f64,
|
||||
g: clear_color[1] as f64,
|
||||
b: clear_color[2] as f64,
|
||||
a: clear_color[3] as f64,
|
||||
}),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
depth_slice: None,
|
||||
})],
|
||||
depth_stencil_attachment: self.depth_texture_view.get(&viewport_id).map(|view| {
|
||||
wgpu::RenderPassDepthStencilAttachment {
|
||||
view,
|
||||
depth_ops: self
|
||||
.options
|
||||
.depth_stencil_format
|
||||
.is_some_and(|depth_stencil_format| {
|
||||
depth_stencil_format.has_depth_aspect()
|
||||
})
|
||||
.then_some(wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(1.0),
|
||||
// It is very unlikely that the depth buffer is needed after egui finished rendering
|
||||
// so no need to store it. (this can improve performance on tiling GPUs like mobile chips or Apple Silicon)
|
||||
store: wgpu::StoreOp::Discard,
|
||||
}),
|
||||
stencil_ops: self
|
||||
.options
|
||||
.depth_stencil_format
|
||||
.is_some_and(|depth_stencil_format| {
|
||||
depth_stencil_format.has_stencil_aspect()
|
||||
})
|
||||
.then_some(wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(0),
|
||||
store: wgpu::StoreOp::Discard,
|
||||
}),
|
||||
}
|
||||
}),
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
multiview_mask: None,
|
||||
});
|
||||
|
||||
// Forgetting the pass' lifetime means that we are no longer compile-time protected from
|
||||
// runtime errors caused by accessing the parent encoder before the render pass is dropped.
|
||||
// Since we don't pass it on to the renderer, we should be perfectly safe against this mistake here!
|
||||
renderer.render(
|
||||
&mut render_pass.forget_lifetime(),
|
||||
clipped_primitives,
|
||||
&screen_descriptor,
|
||||
);
|
||||
|
||||
if capture && let Some(capture_state) = &mut self.screen_capture_state {
|
||||
capture_buffer = Some(capture_state.copy_textures(
|
||||
&render_state.device,
|
||||
&output_frame,
|
||||
&mut encoder,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let encoded = {
|
||||
profiling::scope!("CommandEncoder::finish");
|
||||
encoder.finish()
|
||||
};
|
||||
|
||||
// Submit the commands: both the main buffer and user-defined ones.
|
||||
{
|
||||
profiling::scope!("Queue::submit");
|
||||
// wgpu doesn't document where vsync can happen. Maybe here?
|
||||
let start = web_time::Instant::now();
|
||||
render_state
|
||||
.queue
|
||||
.submit(core::iter::chain(user_cmd_bufs, [encoded]));
|
||||
vsync_sec += start.elapsed().as_secs_f32();
|
||||
};
|
||||
|
||||
// Ensure that the queue guard does not do unnecessary work when dropped
|
||||
render_queue_guard.commands_submitted = true;
|
||||
|
||||
// Free textures marked for destruction **after** queue submit since they might still be used in the current frame.
|
||||
// Calling `wgpu::Texture::destroy` on a texture that is still in use would invalidate the command buffer(s) it is used in.
|
||||
// However, once we called `wgpu::Queue::submit`, it is up for wgpu to determine how long the underlying gpu resource has to live.
|
||||
{
|
||||
let mut renderer = render_state.renderer.write();
|
||||
#[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
|
||||
for id in textures_delta.free.drain() {
|
||||
renderer.free_texture(&id);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(capture_buffer) = capture_buffer
|
||||
&& let Some(screen_capture_state) = &mut self.screen_capture_state
|
||||
{
|
||||
screen_capture_state.read_screen_rgba(
|
||||
self.context.clone(),
|
||||
capture_buffer,
|
||||
capture_data,
|
||||
self.capture_tx.clone(),
|
||||
viewport_id,
|
||||
);
|
||||
}
|
||||
|
||||
window.pre_present_notify();
|
||||
|
||||
{
|
||||
profiling::scope!("present");
|
||||
// wgpu doesn't document where vsync can happen. Maybe here?
|
||||
let start = web_time::Instant::now();
|
||||
render_state.queue.present(output_frame);
|
||||
vsync_sec += start.elapsed().as_secs_f32();
|
||||
}
|
||||
|
||||
vsync_sec
|
||||
}
|
||||
|
||||
/// Call this at the beginning of each frame to receive the requested screenshots.
|
||||
pub fn handle_screenshots(&self, events: &mut Vec<Event>) {
|
||||
for (viewport_id, user_data, screenshot) in self.capture_rx.try_iter() {
|
||||
let screenshot = Arc::new(screenshot);
|
||||
for data in user_data {
|
||||
events.push(Event::Screenshot {
|
||||
viewport_id,
|
||||
user_data: data,
|
||||
image: Arc::clone(&screenshot),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gc_viewports(&mut self, active_viewports: &ViewportIdSet) {
|
||||
self.surfaces.retain(|id, _| active_viewports.contains(id));
|
||||
self.depth_texture_view
|
||||
.retain(|id, _| active_viewports.contains(id));
|
||||
self.msaa_texture_view
|
||||
.retain(|id, _| active_viewports.contains(id));
|
||||
}
|
||||
|
||||
#[expect(clippy::needless_pass_by_ref_mut, clippy::unused_self)]
|
||||
pub fn destroy(&mut self) {
|
||||
// TODO(emilk): something here?
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user