egui_d3d11/
app.rs

1use crate::{
2    backup::BackupState,
3    input::{InputCollector, InputResult},
4    mesh::{create_index_buffer, create_vertex_buffer, GpuMesh, GpuVertex},
5    shader::CompiledShaders,
6    texture::TextureAllocator,
7};
8use clipboard::{windows_clipboard::WindowsClipboardContext, ClipboardProvider};
9use egui::{epaint::Primitive, Context};
10use once_cell::sync::OnceCell;
11use std::{mem::size_of, ops::DerefMut};
12use windows::{
13    core::HRESULT,
14    Win32::{
15        Foundation::{HWND, LPARAM, RECT, WPARAM},
16        Graphics::{
17            Direct3D::D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST,
18            Direct3D11::{
19                ID3D11Device, ID3D11DeviceContext, ID3D11InputLayout, ID3D11RenderTargetView,
20                ID3D11Texture2D, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_BLEND_DESC,
21                D3D11_BLEND_INV_SRC_ALPHA, D3D11_BLEND_ONE, D3D11_BLEND_OP_ADD,
22                D3D11_BLEND_SRC_ALPHA, D3D11_COLOR_WRITE_ENABLE_ALL, D3D11_COMPARISON_ALWAYS,
23                D3D11_CULL_NONE, D3D11_FILL_SOLID, D3D11_FILTER_MIN_MAG_MIP_LINEAR,
24                D3D11_INPUT_ELEMENT_DESC, D3D11_INPUT_PER_VERTEX_DATA, D3D11_RASTERIZER_DESC,
25                D3D11_RENDER_TARGET_BLEND_DESC, D3D11_SAMPLER_DESC, D3D11_TEXTURE_ADDRESS_BORDER,
26                D3D11_VIEWPORT,
27            },
28            Dxgi::{
29                Common::{
30                    DXGI_FORMAT_R32G32B32A32_FLOAT, DXGI_FORMAT_R32G32_FLOAT, DXGI_FORMAT_R32_UINT,
31                },
32                IDXGISwapChain,
33            },
34        },
35        UI::WindowsAndMessaging::GetClientRect,
36    },
37};
38
39#[allow(clippy::type_complexity)]
40struct AppData<T> {
41    render_view: Option<ID3D11RenderTargetView>,
42    ui: Box<dyn FnMut(&Context, &mut T) + 'static>,
43    tex_alloc: TextureAllocator,
44    input_layout: ID3D11InputLayout,
45    input_collector: InputCollector,
46    shaders: CompiledShaders,
47    backup: BackupState,
48    ctx: Context,
49    state: T,
50}
51
52#[cfg(feature = "parking-lot")]
53use parking_lot::{Mutex, MutexGuard};
54#[cfg(feature = "spin-lock")]
55use spin::lock_api::{Mutex, MutexGuard};
56
57use lock_api::MappedMutexGuard;
58
59/// Heart and soul of this integration.
60/// Main methods you are going to use are:
61/// * [`Self::present`] - Should be called inside of hook or before present.
62/// * [`Self::resize_buffers`] - Should be called **INSTEAD** of swapchain's `ResizeBuffers`.
63/// * [`Self::wnd_proc`] - Should be called on each `WndProc`.
64pub struct DirectX11App<T = ()> {
65    data: Mutex<Option<AppData<T>>>,
66    hwnd: OnceCell<HWND>,
67}
68
69impl<T> DirectX11App<T> {
70    const INPUT_ELEMENTS_DESC: [D3D11_INPUT_ELEMENT_DESC; 3] = [
71        D3D11_INPUT_ELEMENT_DESC {
72            SemanticName: pc_str!("POSITION"),
73            SemanticIndex: 0,
74            Format: DXGI_FORMAT_R32G32_FLOAT,
75            InputSlot: 0,
76            AlignedByteOffset: 0,
77            InputSlotClass: D3D11_INPUT_PER_VERTEX_DATA,
78            InstanceDataStepRate: 0,
79        },
80        D3D11_INPUT_ELEMENT_DESC {
81            SemanticName: pc_str!("TEXCOORD"),
82            SemanticIndex: 0,
83            Format: DXGI_FORMAT_R32G32_FLOAT,
84            InputSlot: 0,
85            AlignedByteOffset: D3D11_APPEND_ALIGNED_ELEMENT,
86            InputSlotClass: D3D11_INPUT_PER_VERTEX_DATA,
87            InstanceDataStepRate: 0,
88        },
89        D3D11_INPUT_ELEMENT_DESC {
90            SemanticName: pc_str!("COLOR"),
91            SemanticIndex: 0,
92            Format: DXGI_FORMAT_R32G32B32A32_FLOAT,
93            InputSlot: 0,
94            AlignedByteOffset: D3D11_APPEND_ALIGNED_ELEMENT,
95            InputSlotClass: D3D11_INPUT_PER_VERTEX_DATA,
96            InstanceDataStepRate: 0,
97        },
98    ];
99}
100
101impl<T> DirectX11App<T> {
102    /// Creates new [`DirectX11App`] in const context. You are supposed to create a single static item to store the application state.
103    pub const fn new() -> Self {
104        Self {
105            data: Mutex::new(None),
106            hwnd: OnceCell::new(),
107        }
108    }
109
110    /// Checks if the app is ready to draw and if it's safe to invoke `present`, `wndproc`, etc.
111    /// `true` means that you have already called an `init_*` on the application.
112    pub fn is_ready(&self) -> bool {
113        self.hwnd.get().is_some()
114    }
115
116    /// Initializes application and state. You should call this only once!
117    pub fn init_with_state_context(
118        &self,
119        swap: &IDXGISwapChain,
120        ui: impl FnMut(&Context, &mut T) + 'static,
121        state: T,
122        context: Context,
123    ) {
124        unsafe {
125            if self.hwnd.get().is_some() {
126                panic_msg!("You must call init only once");
127            }
128
129            let hwnd = expect!(swap.GetDesc(), "Failed to get swapchain's descriptor").OutputWindow;
130            if hwnd.0 == -1 {
131                panic_msg!("Invalid output window descriptor");
132            }
133            let _ = self.hwnd.set(hwnd);
134
135            let dev: ID3D11Device = expect!(swap.GetDevice(), "Failed to get swapchain's device");
136
137            let backbuffer: ID3D11Texture2D =
138                expect!(swap.GetBuffer(0), "Failed to get swapchain's backbuffer");
139            let render_view = Some(expect!(
140                dev.CreateRenderTargetView(backbuffer, 0 as _),
141                "Failed to create new render target view"
142            ));
143
144            let shaders = CompiledShaders::new(&dev);
145            let input_layout = expect!(
146                dev.CreateInputLayout(&Self::INPUT_ELEMENTS_DESC, shaders.bytecode()),
147                "Failed to create input layout"
148            );
149
150            *self.data.lock() = Some(AppData {
151                input_collector: InputCollector::new(hwnd),
152                tex_alloc: TextureAllocator::default(),
153                backup: BackupState::default(),
154                ui: Box::new(ui),
155                ctx: context,
156                input_layout,
157                render_view,
158                shaders,
159                state,
160            });
161        }
162    }
163
164    /// Initializes application and state. Sets egui's context to default value. You should call this only once!
165    #[inline]
166    pub fn init_with_state(
167        &self,
168        swap: &IDXGISwapChain,
169        ui: impl FnMut(&Context, &mut T) + 'static,
170        state: T,
171    ) {
172        self.init_with_state_context(swap, ui, state, Context::default())
173    }
174
175    /// Initializes application and state while allowing you to mutate the initial state of the egui's context. You should call this only once!
176    #[inline]
177    pub fn init_with_mutate(
178        &self,
179        swap: &IDXGISwapChain,
180        ui: impl FnMut(&Context, &mut T) + 'static,
181        mut state: T,
182        mutate: impl FnOnce(&mut Context, &mut T),
183    ) {
184        let mut ctx = Context::default();
185        mutate(&mut ctx, &mut state);
186
187        self.init_with_state_context(swap, ui, state, ctx);
188    }
189
190    #[cfg(feature = "parking-lot")]
191    pub fn lock_state(&self) -> MappedMutexGuard<'_, parking_lot::RawMutex, T> {
192        MutexGuard::map(self.data.lock(), |app| &mut app.as_mut().unwrap().state)
193    }
194
195    #[cfg(feature = "spin-lock")]
196    pub fn lock_state(&self) -> MappedMutexGuard<'_, spin::mutex::Mutex<()>, T> {
197        MutexGuard::map(self.data.lock(), |app| &mut app.as_mut().unwrap().state)
198    }
199
200    fn lock_data(&self) -> impl DerefMut<Target = AppData<T>> + '_ {
201        MutexGuard::map(self.data.lock(), |app| {
202            expect!(app.as_mut(), "You need to call init first")
203        })
204    }
205}
206
207impl<T: Default> DirectX11App<T> {
208    /// Initializes application and sets the state to its default value. You should call this only once!
209    #[inline]
210    pub fn init_default(&self, swap: &IDXGISwapChain, ui: impl FnMut(&Context, &mut T) + 'static) {
211        self.init_with_state_context(swap, ui, T::default(), Context::default());
212    }
213}
214
215impl<T> DirectX11App<T> {
216    /// Present call. Should be called once per original present call, before or inside of hook.
217    #[allow(clippy::cast_ref_to_mut)]
218    pub fn present(&self, swap_chain: &IDXGISwapChain) {
219        unsafe {
220            let this = &mut *self.lock_data();
221
222            let (dev, ctx) = &get_device_and_context(swap_chain);
223
224            this.backup.save(ctx);
225
226            let screen = self.get_screen_size();
227
228            if cfg!(feature = "clear") {
229                ctx.ClearRenderTargetView(&this.render_view, [0.39, 0.58, 0.92, 1.].as_ptr());
230            }
231
232            let output = this.ctx.run(this.input_collector.collect_input(), |ctx| {
233                // Dont look here, it should be fine until someone tries to do something horrible.
234                (this.ui)(ctx, &mut this.state);
235            });
236
237            if !output.textures_delta.is_empty() {
238                this.tex_alloc
239                    .process_deltas(dev, ctx, output.textures_delta);
240            }
241
242            if !output.platform_output.copied_text.is_empty() {
243                let _ = WindowsClipboardContext.set_contents(output.platform_output.copied_text);
244            }
245
246            if output.shapes.is_empty() {
247                this.backup.restore(ctx);
248                return;
249            }
250
251            let primitives = this
252                .ctx
253                .tessellate(output.shapes)
254                .into_iter()
255                .filter_map(|prim| {
256                    if let Primitive::Mesh(mesh) = prim.primitive {
257                        GpuMesh::from_mesh(screen, mesh, prim.clip_rect)
258                    } else {
259                        panic!("Paint callbacks are not yet supported")
260                    }
261                })
262                .collect::<Vec<_>>();
263
264            self.set_blend_state(dev, ctx);
265            self.set_raster_options(dev, ctx);
266            self.set_sampler_state(dev, ctx);
267
268            ctx.RSSetViewports(&[self.get_viewport()]);
269            ctx.OMSetRenderTargets(&[this.render_view.clone()], None);
270            ctx.IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
271            ctx.IASetInputLayout(&this.input_layout);
272
273            for mesh in primitives {
274                let idx = create_index_buffer(dev, &mesh);
275                let vtx = create_vertex_buffer(dev, &mesh);
276
277                let texture = this.tex_alloc.get_by_id(mesh.texture_id);
278
279                ctx.RSSetScissorRects(&[RECT {
280                    left: mesh.clip.left() as _,
281                    top: mesh.clip.top() as _,
282                    right: mesh.clip.right() as _,
283                    bottom: mesh.clip.bottom() as _,
284                }]);
285
286                if texture.is_some() {
287                    ctx.PSSetShaderResources(0, &[texture]);
288                }
289
290                ctx.IASetVertexBuffers(0, 1, &Some(vtx), &(size_of::<GpuVertex>() as _), &0);
291                ctx.IASetIndexBuffer(idx, DXGI_FORMAT_R32_UINT, 0);
292                ctx.VSSetShader(&this.shaders.vertex, &[]);
293                ctx.PSSetShader(&this.shaders.pixel, &[]);
294
295                ctx.DrawIndexed(mesh.indices.len() as _, 0, 0);
296            }
297
298            this.backup.restore(ctx);
299        }
300    }
301
302    /// Call when resizing buffers.
303    /// Do not call the original function before it, instead call it inside of the `original` closure.
304    /// # Behavior
305    /// In `origin` closure make sure to call the original `ResizeBuffers`.
306    pub fn resize_buffers(
307        &self,
308        swap_chain: &IDXGISwapChain,
309        original: impl FnOnce() -> HRESULT,
310    ) -> HRESULT {
311        unsafe {
312            let this = &mut *self.lock_data();
313            drop(this.render_view.take());
314
315            let result = original();
316
317            let backbuffer: ID3D11Texture2D = expect!(
318                swap_chain.GetBuffer(0),
319                "Failed to get swapchain's backbuffer"
320            );
321
322            let device: ID3D11Device =
323                expect!(swap_chain.GetDevice(), "Failed to get swapchain's device");
324
325            let new_view = expect!(
326                device.CreateRenderTargetView(backbuffer, 0 as _),
327                "Failed to create render target view"
328            );
329
330            this.render_view = Some(new_view);
331            result
332        }
333    }
334
335    /// Call on each `WndProc` occurence.
336    /// Returns `true` if message was recognized and dispatched by input handler,
337    /// `false` otherwise.
338    #[inline]
339    pub fn wnd_proc(&self, umsg: u32, wparam: WPARAM, lparam: LPARAM) -> InputResult {
340        self.lock_data()
341            .input_collector
342            .process(umsg, wparam.0, lparam.0)
343    }
344}
345
346impl<T> DirectX11App<T> {
347    #[inline]
348    fn get_screen_size(&self) -> (f32, f32) {
349        let mut rect = RECT::default();
350        unsafe {
351            GetClientRect(
352                expect!(self.hwnd.get(), "You need to call init first"),
353                &mut rect,
354            );
355        }
356        (
357            (rect.right - rect.left) as f32,
358            (rect.bottom - rect.top) as f32,
359        )
360    }
361
362    #[inline]
363    fn get_viewport(&self) -> D3D11_VIEWPORT {
364        let (w, h) = self.get_screen_size();
365        D3D11_VIEWPORT {
366            TopLeftX: 0.,
367            TopLeftY: 0.,
368            Width: w,
369            Height: h,
370            MinDepth: 0.,
371            MaxDepth: 1.,
372        }
373    }
374
375    fn set_blend_state(&self, dev: &ID3D11Device, ctx: &ID3D11DeviceContext) {
376        let mut targets: [D3D11_RENDER_TARGET_BLEND_DESC; 8] = Default::default();
377        targets[0].BlendEnable = true.into();
378        targets[0].SrcBlend = D3D11_BLEND_SRC_ALPHA;
379        targets[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA;
380        targets[0].BlendOp = D3D11_BLEND_OP_ADD;
381        targets[0].SrcBlendAlpha = D3D11_BLEND_ONE;
382        targets[0].DestBlendAlpha = D3D11_BLEND_INV_SRC_ALPHA;
383        targets[0].BlendOpAlpha = D3D11_BLEND_OP_ADD;
384        targets[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL.0 as _;
385
386        let blend_desc = D3D11_BLEND_DESC {
387            AlphaToCoverageEnable: false.into(),
388            IndependentBlendEnable: false.into(),
389            RenderTarget: targets,
390        };
391
392        unsafe {
393            let blend_state = expect!(
394                dev.CreateBlendState(&blend_desc),
395                "Failed to create blend state"
396            );
397            ctx.OMSetBlendState(&blend_state, [0., 0., 0., 0.].as_ptr(), 0xffffffff);
398        }
399    }
400
401    fn set_raster_options(&self, dev: &ID3D11Device, ctx: &ID3D11DeviceContext) {
402        let raster_desc = D3D11_RASTERIZER_DESC {
403            FillMode: D3D11_FILL_SOLID,
404            CullMode: D3D11_CULL_NONE,
405            FrontCounterClockwise: false.into(),
406            DepthBias: false.into(),
407            DepthBiasClamp: 0.,
408            SlopeScaledDepthBias: 0.,
409            DepthClipEnable: false.into(),
410            ScissorEnable: true.into(),
411            MultisampleEnable: false.into(),
412            AntialiasedLineEnable: false.into(),
413        };
414
415        unsafe {
416            let options = expect!(
417                dev.CreateRasterizerState(&raster_desc),
418                "Failed to create rasterizer state"
419            );
420            ctx.RSSetState(&options);
421        }
422    }
423
424    fn set_sampler_state(&self, dev: &ID3D11Device, ctx: &ID3D11DeviceContext) {
425        let desc = D3D11_SAMPLER_DESC {
426            Filter: D3D11_FILTER_MIN_MAG_MIP_LINEAR,
427            AddressU: D3D11_TEXTURE_ADDRESS_BORDER,
428            AddressV: D3D11_TEXTURE_ADDRESS_BORDER,
429            AddressW: D3D11_TEXTURE_ADDRESS_BORDER,
430            MipLODBias: 0.,
431            ComparisonFunc: D3D11_COMPARISON_ALWAYS,
432            MinLOD: 0.,
433            MaxLOD: 0.,
434            BorderColor: [1., 1., 1., 1.],
435            ..Default::default()
436        };
437
438        unsafe {
439            let sampler = expect!(dev.CreateSamplerState(&desc), "Failed to create sampler");
440            ctx.PSSetSamplers(0, &[Some(sampler)]);
441        }
442    }
443}
444
445unsafe fn get_device_and_context(swap: &IDXGISwapChain) -> (ID3D11Device, ID3D11DeviceContext) {
446    let device: ID3D11Device = expect!(swap.GetDevice(), "Failed to get swapchain's device");
447    let mut ctx = None;
448    device.GetImmediateContext(&mut ctx);
449    (
450        device,
451        expect!(ctx, "Failed to get device's immediate context"),
452    )
453}