agdk-egui: use upstream egui-winit + egui-wgpu crates

The functionality found in egui_winit_platform and egui_wgpu_backend is
now available in the upstream egui-winit and egui-wgpu crates
respectively.

This has simplified the example itself and also removed the dependency
on epi.

The example is now based on egui 0.18 (was previously 0.17)

Since the upstream egui-winit/wgpu crates were generally assuming that
the graphics context and surface can be initialized immediately this
currently builds against a branch that aims to upstream changes that
remove these assumptions:

https://github.com/rib/egui/tree/android-deferred-winit-wgpu
Ref: https://github.com/emilk/egui/pull/1634
This commit is contained in:
Robert Bragg
2022-05-17 18:14:18 +01:00
parent bfc0d091e6
commit 69d0de4db1
3 changed files with 88 additions and 373 deletions
+12 -9
View File
@@ -8,20 +8,23 @@ resolver = "2"
[dependencies]
log = "0.4"
winit = { git = "https://github.com/rib/winit", branch = "agdk-game-activity" }
#winit = { path="../../../winit" }
winit = "0.26"
wgpu = "0.12.0"
pollster = "0.2"
#egui_winit_platform = "0.14"
egui_winit_platform = { path = "../../../egui_winit_platform" }
egui_wgpu_backend = "0.17"
egui = "0.17"
chrono = "0.4"
epi = "0.17"
egui_demo_lib = "0.17"
egui = "0.18"
egui-wgpu = { version = "0.18", features = [ "winit" ] }
egui-winit = { version = "0.18", default-features = false }
egui_demo_lib = "0.18"
[patch.crates-io]
winit = { git = "https://github.com/rib/winit", branch = "agdk-game-activity" }
#winit = { path="../../../winit" }
egui = { git = "https://github.com/rib/egui", branch = "android-deferred-winit-wgpu" }
egui-winit = { git = "https://github.com/rib/egui", branch = "android-deferred-winit-wgpu" }
egui-wgpu = { git = "https://github.com/rib/egui", branch = "android-deferred-winit-wgpu" }
#egui = { path = "../../../egui/egui" }
#egui-wgpu = { path = "../../../egui/egui-wgpu" }
#egui-winit = { path = "../../../egui/egui-winit" }
[target.'cfg(not(target_os = "android"))'.dependencies]
env_logger = "0.9"
+3
View File
@@ -3,6 +3,9 @@ This tests using the game_activity crate with egui, winit and wgpu.
This is based on a re-worked winit backend here:
https://github.com/rib/winit/tree/agdk-game-activity
and based on updated egui-winit and egui-wgpu crates here:
https://github.com/rib/egui/tree/android-deferred-winit-wgpu
```
rustup target add aarch64-linux-android
+73 -364
View File
@@ -1,428 +1,137 @@
use log::Level;
use log::{trace, warn, error};
use wgpu::TextureFormat;
use wgpu::{Instance, Adapter, Device, Queue};
use winit::event_loop::{EventLoopWindowTarget, EventLoopBuilder};
use std::ops::Deref;
use std::sync::{Arc, RwLock};
use winit::{
event_loop::{ControlFlow},
};
use std::iter;
use std::time::Instant;
use chrono::Timelike;
use egui::FontDefinitions;
use egui_wgpu_backend::{RenderPass, ScreenDescriptor};
use egui_winit_platform::{Platform, PlatformDescriptor};
use epi::{*, App};
use egui_wgpu::winit::Painter;
use egui_winit::State;
//use egui_winit_platform::{Platform, PlatformDescriptor};
use winit::event::Event::*;
const INITIAL_WIDTH: u32 = 1920;
const INITIAL_HEIGHT: u32 = 1080;
struct RenderState {
device: Device,
queue: Queue,
target_format: TextureFormat,
egui_rpass: RwLock<RenderPass>,
}
struct SurfaceState {
window: winit::window::Window,
surface: wgpu::Surface
}
struct AppInner {
instance: Instance,
adapter: Option<Adapter>,
surface_state: Option<SurfaceState>,
render_state: Option<RenderState>,
}
struct EguiApp {
inner: Arc<RwLock<AppInner>>
}
impl EguiApp {
fn new(instance: Instance) -> Self {
Self {
inner: Arc::new(RwLock::new(AppInner {
instance,
adapter: None,
surface_state: None,
render_state: None,
}))
}
}
}
impl Deref for EguiApp {
type Target = Arc<RwLock<AppInner>>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
/// A custom event type for the winit app.
enum Event {
RequestRedraw,
}
/// This is the repaint signal type that egui needs for requesting a repaint from another thread.
/// It sends the custom RequestRedraw event to the winit event loop.
struct ExampleRepaintSignal(std::sync::Mutex<winit::event_loop::EventLoopProxy<Event>>);
impl epi::backend::RepaintSignal for ExampleRepaintSignal {
fn request_repaint(&self) {
self.0.lock().unwrap().send_event(Event::RequestRedraw).ok();
}
}
/// Enable egui to request redraws via a custom Winit event...
#[derive(Clone)]
struct RepaintSignal(std::sync::Arc<std::sync::Mutex<winit::event_loop::EventLoopProxy<Event>>>);
/// Time of day as seconds since midnight. Used for clock in demo app.
pub fn seconds_since_midnight() -> f64 {
let time = chrono::Local::now().time();
time.num_seconds_from_midnight() as f64 + 1e-9 * (time.nanosecond() as f64)
}
async fn init_render_state(adapter: &Adapter, target_format: TextureFormat) -> RenderState {
trace!("Initializing render state");
trace!("WGPU: requesting device");
// Create the logical device and command queue
let (device, queue) = adapter
.request_device(
&wgpu::DeviceDescriptor {
label: None,
features: wgpu::Features::empty(),
// Make sure we use the texture resolution limits from the adapter, so we can support images the size of the swapchain.
limits: wgpu::Limits::downlevel_webgl2_defaults()
.using_resolution(adapter.limits()),
},
None,
)
.await
.expect("Failed to create device");
let egui_rpass = RenderPass::new(&device, target_format, 1);
RenderState {
device,
queue,
target_format,
egui_rpass: RwLock::new(egui_rpass),
}
}
fn configure_surface_swapchain(render_state: &RenderState, surface_state: &SurfaceState) {
let swapchain_format = render_state.target_format;
let size = surface_state.window.inner_size();
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: swapchain_format,
width: size.width,
height: size.height,
present_mode: wgpu::PresentMode::Mailbox,
//present_mode: wgpu::PresentMode::Fifo,
};
trace!("WGPU: Configuring surface swapchain: format = {swapchain_format:?}, size = {size:?}");
surface_state.surface.configure(&render_state.device, &config);
}
// We want to defer the initialization of our render state until
// we have a surface so we can take its format into account.
//
// After we've initialized our render state once though we
// expect all future surfaces will have the same format and we
// so this stat will remain valid.
async fn ensure_render_state_for_surface(app: &EguiApp, new_surface_state: &SurfaceState) {
let mut app_guard = app.inner.write().unwrap();
if app_guard.adapter.is_none() {
trace!("WGPU: requesting a suitable adapter (compatible with our surface)");
let adapter = app_guard.instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
force_fallback_adapter: false,
// Request an adapter which can render to our surface
compatible_surface: Some(&new_surface_state.surface),
})
.await
.expect("Failed to find an appropriate adapter");
app_guard.adapter = Some(adapter);
}
let adapter = app_guard.adapter.as_ref().unwrap();
if app_guard.render_state.is_none() {
trace!("WGPU: finding preferred swapchain format");
let swapchain_format = new_surface_state.surface.get_preferred_format(&adapter).unwrap();
let rs = init_render_state(adapter, swapchain_format).await;
app_guard.render_state = Some(rs);
}
}
fn create_surface<T>(app: &EguiApp, event_loop: &EventLoopWindowTarget<T>) -> SurfaceState {
//let window = winit::window::Window::new(&event_loop).unwrap();
fn create_window<T>(event_loop: &EventLoopWindowTarget<T>, state: &mut State, painter: &mut Painter) -> winit::window::Window {
let window = winit::window::WindowBuilder::new()
.with_decorations(true)
.with_resizable(true)
.with_transparent(false)
.with_title("egui-wgpu_winit example")
.with_title("egui winit + wgpu example")
.with_inner_size(winit::dpi::PhysicalSize {
width: INITIAL_WIDTH,
height: INITIAL_HEIGHT,
})
.build(&event_loop)
.unwrap();
trace!("WGPU: creating surface for native window");
let guard = app.inner.read().unwrap();
let surface = unsafe { guard.instance.create_surface(&window) };
SurfaceState {
window,
surface
unsafe { painter.set_window(Some(&window)) };
// NB: calling set_window will lazily initialize render state which
// means we will be able to query the maximum supported texture
// dimensions
if let Some(max_size) = painter.max_texture_side() {
state.set_max_texture_side(max_size);
}
}
fn resume<T>(app: &EguiApp, event_loop: &EventLoopWindowTarget<T>, platform: &mut Platform) {
trace!("Resumed, creating render state...");
let pixels_per_point = window.scale_factor() as f32;
state.set_pixels_per_point(pixels_per_point);
let new_surface = create_surface(&app, event_loop);
window.request_redraw();
pollster::block_on(ensure_render_state_for_surface(&app, &new_surface));
app.write().unwrap().surface_state = Some(new_surface);
let guard = app.read().unwrap();
let render_state = guard.render_state.as_ref().unwrap();
let surface_state = guard.surface_state.as_ref().unwrap();
configure_surface_swapchain(render_state, surface_state);
let mut size = surface_state.window.inner_size();
let scale_factor = surface_state.window.scale_factor() as f32;
let raw_input = platform.raw_input_mut();
raw_input.pixels_per_point = Some(scale_factor);
raw_input.screen_rect = Some(egui::Rect::from_min_size(
egui::Pos2::default(),
egui::vec2(
size.width as f32,
size.height as f32,
) / scale_factor,
));
// WORKAROUND
//
// egui_winit_platform maintains a private scale_factor that's normally
// only possible to initialize via `Platform::new()` which isn't possible
// on Android before we have a Window. Currently the only way we can update this
// state is by issuing a fake ScaleFactorChanged event...
//
// See: https://github.com/hasenbanck/egui_winit_platform/issues/40
platform.handle_event::<Event>(&winit::event::Event::WindowEvent {
window_id: surface_state.window.id(),
event: winit::event::WindowEvent::ScaleFactorChanged {
scale_factor: scale_factor as f64,
new_inner_size: &mut size }
});
trace!("Making Redraw Request");
surface_state.window.request_redraw();
window
}
fn _main() {
let instance = wgpu::Instance::new(wgpu::Backends::PRIMARY);
//let instance = wgpu::Instance::new(wgpu::Backends::VULKAN);
//let instance = wgpu::Instance::new(wgpu::Backends::GL);
let app = EguiApp::new(instance);
let event_loop = EventLoopBuilder::with_user_event().build();
let repaint_signal = std::sync::Arc::new(ExampleRepaintSignal(std::sync::Mutex::new(
event_loop.create_proxy(),
let ctx = egui::Context::default();
let repaint_signal = RepaintSignal(std::sync::Arc::new(std::sync::Mutex::new(
event_loop.create_proxy()
)));
// We use the egui_winit_platform crate as the platform.
// Note: we are initializing with dummy state because we don't technically know
// anything about the physical size or required scale factor yet...
let mut platform = Platform::new(PlatformDescriptor {
physical_width: INITIAL_WIDTH,
physical_height: INITIAL_HEIGHT,
scale_factor: 1.0,
font_definitions: FontDefinitions::default(),
style: Default::default(),
ctx.set_request_repaint_callback(move || {
repaint_signal.0.lock().unwrap().send_event(Event::RequestRedraw).ok();
});
// We use the egui_wgpu_backend crate as the render backend.
let mut state = State::new(&event_loop);
let mut painter = Painter::new(wgpu::PowerPreference::LowPower, wgpu::PresentMode::Fifo, 1);
let mut window: Option<winit::window::Window> = None;
let mut egui_demo_windows = egui_demo_lib::DemoWindows::default();
// Display the demo application that ships with egui.
let mut demo_app = egui_demo_lib::WrapApp::default();
// On most platforms we can immediately create a winit window. On Android we manage
// window + surface state according to Resumed/Paused events.
#[cfg(not(target_os="android"))]
{
window = Some(create_window(&event_loop, &mut state, &mut painter));
}
let start_time = Instant::now();
let mut previous_frame_time = None;
event_loop.run(move |event, event_loop, control_flow| {
// Pass the winit events to the platform integration.
platform.handle_event(&event);
match event {
NewEvents(winit::event::StartCause::Init) => {
// Note: that because Winit doesn't currently support lifecycle events consistently
// across platforms then we effectively issue a fake 'resume' on non-android
// platforms...
#[cfg(not(target_os="android"))]
resume(&app, event_loop, &mut platform)
}
#[cfg(target_os="android")]
Resumed => {
resume(&app, event_loop, &mut platform);
match window {
None => {
window = Some(create_window(event_loop, &mut state, &mut painter));
}
Some(ref window) => {
unsafe { painter.set_window(Some(window)) };
window.request_redraw();
}
}
}
#[cfg(target_os="android")]
Suspended => {
window = None;
}
RedrawRequested(..) => {
let guard = app.read().unwrap();
if let Some(window) = window.as_ref() {
let raw_input = state.take_egui_input(window);
if let Some(ref surface_state) = guard.surface_state {
if let Some(ref render_state) = guard.render_state {
platform.update_time(start_time.elapsed().as_secs_f64());
let full_output = ctx.run(raw_input, |ctx| {
egui_demo_windows.ui(ctx);
});
state.handle_platform_output(window, &ctx, full_output.platform_output);
let size = surface_state.window.inner_size();
let scale_factor = surface_state.window.scale_factor() as f32;
painter.paint_and_update_textures(state.pixels_per_point(),
egui::Rgba::default(),
&ctx.tessellate(full_output.shapes),
&full_output.textures_delta);
let output_frame = match surface_state.surface.get_current_texture() {
Ok(frame) => frame,
Err(wgpu::SurfaceError::Outdated) => {
// This error occurs when the app is minimized on Windows.
// Silently return here to prevent spamming the console with:
// "The underlying surface has changed, and therefore the swap chain must be updated"
return;
}
Err(e) => {
eprintln!("Dropped frame with error: {}", e);
return;
}
};
let output_view = output_frame
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
// Begin to draw the UI frame.
let egui_start = Instant::now();
platform.begin_frame();
let app_output = epi::backend::AppOutput::default();
let mut frame = epi::Frame::new(epi::backend::FrameData {
info: epi::IntegrationInfo {
name: "egui_example",
web_info: None,
cpu_usage: previous_frame_time,
native_pixels_per_point: Some(scale_factor),
prefer_dark_mode: None,
},
output: app_output,
repaint_signal: repaint_signal.clone(),
});
// Draw the demo application.
demo_app.update(&platform.context(), &mut frame);
// End the UI frame. We could now handle the output and draw the UI with the backend.
//let (_output, paint_commands) =
let output = platform.end_frame(Some(&surface_state.window));
let paint_jobs = platform.context().tessellate(output.shapes);
let frame_time = (Instant::now() - egui_start).as_secs_f64() as f32;
previous_frame_time = Some(frame_time);
let mut encoder = render_state.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("encoder"),
});
// Upload all resources for the GPU.
let screen_descriptor = ScreenDescriptor {
physical_width: size.width,
physical_height: size.height,
scale_factor: scale_factor,
};
{
let mut egui_rpass = render_state.egui_rpass.write().unwrap();
match egui_rpass.add_textures(&render_state.device, &render_state.queue, &output.textures_delta) {
Ok(()) => {
//egui_rpass.update_texture(&device, &queue, &platform.context().font_image());
//egui_rpass.update_user_textures(&device, &queue);
egui_rpass.update_buffers(&render_state.device, &render_state.queue, &paint_jobs, &screen_descriptor);
// Record all render passes.
egui_rpass
.execute(
&mut encoder,
&output_view,
&paint_jobs,
&screen_descriptor,
Some(wgpu::Color::BLACK),
)
.unwrap();
// Submit the commands.
render_state.queue.submit(iter::once(encoder.finish()));
// Redraw egui
output_frame.present();
if let Err(err) = egui_rpass.remove_textures(output.textures_delta) {
error!("Failed to remove texture deltas from Egui render pass: {err:?}");
}
}
Err(err) => {
error!("Failed to add texture deltas before executing render pass: {err:?}");
}
}
}
// Suppport reactive on windows only, but not on linux.
// if _output.needs_repaint {
// *control_flow = ControlFlow::Poll;
// } else {
// *control_flow = ControlFlow::Wait;
// }
if full_output.needs_repaint {
window.request_redraw();
}
}
}
MainEventsCleared | UserEvent(Event::RequestRedraw) => {
let guard = app.read().unwrap();
if let Some(ref surface_state) = guard.surface_state {
surface_state.window.request_redraw();
if let Some(window) = window.as_ref() {
window.request_redraw();
}
}
WindowEvent { event, .. } => match event {
winit::event::WindowEvent::Resized(size) => {
let guard = app.read().unwrap();
if let Some(ref surface_state) = guard.surface_state {
if let Some(ref render_state) = guard.render_state {
warn!("resized size = {size:?}, window inner_size = {:?}", surface_state.window.inner_size());
// BUG WORKAROUND:
//
// Currently Windows will emit an erroneous Resized 0x0 event on minimize which doesn't
// even logically make sense on Windows, where apps can continue to render for minimization
// thumbnails. A 0x0 size will almost certainly lead to divide zeros when calculating
// aspect ratios so we just ignore the event.
//
// See: https://github.com/rust-windowing/winit/issues/208
if size.width > 0 && size.height > 0 {
configure_surface_swapchain(render_state, surface_state);
}
WindowEvent { event, .. } => {
if state.on_event(&ctx, &event) == false {
match event {
winit::event::WindowEvent::Resized(size) => {
painter.on_window_resized(size.width, size.height);
}
winit::event::WindowEvent::CloseRequested => {
*control_flow = ControlFlow::Exit;
}
_ => {}
}
}
winit::event::WindowEvent::CloseRequested => {
*control_flow = ControlFlow::Exit;
}
_ => {}
},
_ => (),
}