Fix desktop launch, app icons and keyboard parameter editing

This commit is contained in:
bherbruck committed 2026-10-11 19:09:47 -04:00
1 parent 8fa52c8e5e
commit 6dd202d277
45 files changed
+5993 -272

No files matched your search

+11
View File
@@ -61,3 +61,14 @@ Changes:
move both bodies and the result back) hung the app.
- `lib.rs`: compiler warnings no longer fail the build. Test targets are dropped (their sources
are not vendored).
## egui-wgpu 0.36.2 (MIT OR Apache-2.0, Emil Ernerfeldt and contributors)
Original package sources from https://github.com/emilk/egui/tree/0.36.2/crates/egui-wgpu,
with both original licence texts retained. No images or bundled documentation are copied.
SolidCraft changes:
- `src/lib.rs`: use 1x MSAA on the selected OpenGL adapter before creating pipelines,
exposing the effective sample count so host paint callbacks can match it.
- `src/winit.rs`: use that same effective count for depth and resolve attachments.
- Other backends retain requested MSAA, without another adapter or device probe.
+339
View File
@@ -0,0 +1,339 @@
# 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"
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
View File
@@ -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 }
+201
View File
@@ -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.
+25
View File
@@ -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.
+267
View File
@@ -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,
}
}
}
+162
View File
@@ -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;
}
+585
View File
@@ -0,0 +1,585 @@
//! 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,
/// Effective sample count after backend compatibility handling.
/// Custom paint callbacks must use this, rather than the requested setting.
pub msaa_samples: u32,
/// 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)?;
// SolidCraft: the OpenGL resolve path can present an entirely black frame with MSAA
// (notably Mesa/llvmpipe). Decide after selecting the real surface adapter,
// before creating either pipelines or attachments; no second GPU probe.
let options = RendererOptions {
msaa_samples: effective_msaa_samples(adapter.get_info().backend, options.msaa_samples),
..options
};
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,
msaa_samples: options.msaa_samples,
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",
}
}
/// Selected-adapter policy shared by egui pipelines, attachments and host callbacks.
pub fn effective_msaa_samples(backend: wgpu::Backend, requested: u32) -> u32 {
if backend == wgpu::Backend::Gl { 1 } else { requested.max(1) }
}
#[cfg(test)]
mod msaa_tests {
#[test]
fn gl_saved_multisampling_is_clamped_and_native_backends_keep_it() {
for requested in [0, 1, 2, 4, 8] {
assert_eq!(super::effective_msaa_samples(wgpu::Backend::Gl, requested), 1);
for backend in [wgpu::Backend::Vulkan, wgpu::Backend::Dx12, wgpu::Backend::Metal, wgpu::Backend::BrowserWebGpu] {
assert_eq!(super::effective_msaa_samples(backend, requested), requested.max(1));
}
}
}
}
+1178
View File
File diff suppressed because it is too large. Load diff
+331
View File
@@ -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
View File
@@ -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);
}
+802
View File
@@ -0,0 +1,802 @@
#![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,
resizing: bool,
needs_reconfigure: bool,
needs_recreate: bool,
}
/// 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))?;
self.install_surface(
surface,
viewport_id,
old_state.width,
old_state.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?;
// SolidCraft: attachment sample counts must match the backend-adjusted pipelines.
self.options.msaa_samples = render_state.msaa_samples;
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
}
};
self.surfaces.insert(
viewport_id,
SurfaceState {
surface,
width,
height,
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)
}
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!();
let width = width_in_pixels.get();
let height = height_in_pixels.get();
let render_state = self.render_state.as_ref().unwrap();
let surface_state = self.surfaces.get_mut(&viewport_id).unwrap();
surface_state.width = width;
surface_state.height = height;
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.
let screen_descriptor = renderer::ScreenDescriptor {
size_in_pixels: [surface_state.width, surface_state.height],
pixels_per_point,
};
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?
}
}