egui_directx11/
lib.rs

1#![warn(missing_docs)]
2
3//! `egui-directx11`: a Direct3D11 renderer for [`egui`](https://crates.io/crates/egui).
4//!
5//! This crate aims to provide a *minimal* set of features and APIs to render
6//! outputs from `egui` using Direct3D11. We assume you to be familiar with
7//! developing graphics applications using Direct3D11, and if not, this crate is
8//! not likely useful for you. Besides, this crate cares only about rendering
9//! outputs from `egui`, so it is all *your* responsibility to handle things
10//! like setting up the window and event loop, creating the device and swap
11//! chain, etc.
12//!
13//! This crate is built upon the *official* Rust bindings of Direct3D11 and DXGI
14//! APIs from the [`windows`](https://crates.io/crates/windows) crate [maintained by
15//! Microsoft](https://github.com/microsoft/windows-rs). Using this crate with
16//! other Direct3D11 bindings is not recommended and may result in unexpected
17//! behavior.
18//!
19//! To get started, you can check the [`Renderer`] struct provided by this
20//! crate. You can also take a look at the [example](https://github.com/Nekomaru-PKU/egui-directx11/blob/main/examples/main.rs), which demonstrates all you need to do to set up a minimal application
21//! with Direct3D11 and `egui`. This example uses `winit` for window management
22//! and event handling, while native Win32 APIs should also work well.
23
24mod texture;
25use texture::TexturePool;
26
27use std::mem;
28
29const fn zeroed<T>() -> T {
30    unsafe { mem::zeroed() }
31}
32
33use egui::{
34    ClippedPrimitive, Pos2,
35    epaint::{ClippedShape, Primitive, Vertex, textures::TexturesDelta},
36};
37
38use windows::Win32::Foundation::RECT;
39use windows::Win32::Graphics::{Direct3D::*, Direct3D11::*, Dxgi::Common::*};
40use windows::core::BOOL;
41use windows::core::{Interface, Result};
42
43/// The core of this crate. You can set up a renderer via [`Renderer::new`]
44/// and render the output from `egui` with [`Renderer::render`].
45pub struct Renderer {
46    device: ID3D11Device,
47    input_layout: ID3D11InputLayout,
48    vertex_shader: ID3D11VertexShader,
49    pixel_shader: ID3D11PixelShader,
50    rasterizer_state: ID3D11RasterizerState,
51    sampler_state: ID3D11SamplerState,
52    blend_state: ID3D11BlendState,
53
54    texture_pool: TexturePool,
55}
56
57/// Part of [`egui::FullOutput`] that is consumed by [`Renderer::render`].
58///
59/// Call to [`egui::Context::run`] or [`egui::Context::end_frame`] yields a
60/// [`egui::FullOutput`]. The platform integration (for example `egui_winit`)
61/// consumes [`egui::FullOutput::platform_output`] and
62/// [`egui::FullOutput::viewport_output`], and the renderer consumes the rest.
63///
64/// To conveniently split a [`egui::FullOutput`] into a [`RendererOutput`] and
65/// outputs for the platform integration, use [`split_output`].
66#[allow(missing_docs)]
67pub struct RendererOutput {
68    pub textures_delta: TexturesDelta,
69    pub shapes: Vec<ClippedShape>,
70    pub pixels_per_point: f32,
71}
72
73/// Convenience method to split a [`egui::FullOutput`] into the
74/// [`RendererOutput`] part and other parts for platform integration.
75///
76/// The returned tuple should be destructured as:
77/// ```ignore
78/// let (renderer_output, platform_output, viewport_output) =
79///     egui_directx11::split_output(full_output);
80/// ```
81pub fn split_output(
82    full_output: egui::FullOutput,
83) -> (
84    RendererOutput,
85    egui::PlatformOutput,
86    egui::OrderedViewportIdMap<egui::ViewportOutput>,
87) {
88    (
89        RendererOutput {
90            textures_delta: full_output.textures_delta,
91            shapes: full_output.shapes,
92            pixels_per_point: full_output.pixels_per_point,
93        },
94        full_output.platform_output,
95        full_output.viewport_output,
96    )
97}
98
99#[repr(C)]
100struct VertexData {
101    pos: Pos2,
102    uv: Pos2,
103    color: [f32; 4],
104}
105
106struct MeshData {
107    vtx: Vec<VertexData>,
108    idx: Vec<u32>,
109    tex: egui::TextureId,
110    clip_rect: egui::Rect,
111}
112
113impl Renderer {
114    /// Create a [`Renderer`] using the provided Direct3D11 device. The
115    /// [`Renderer`] holds various Direct3D11 resources and states derived
116    /// from the device.
117    ///
118    /// If any Direct3D resource creation fails, this function will return an
119    /// error. You can create the Direct3D11 device with debug layer enabled
120    /// to find out details on the error.
121    pub fn new(device: &ID3D11Device) -> Result<Self> {
122        let mut input_layout = None;
123        let mut vertex_shader = None;
124        let mut pixel_shader = None;
125        let mut rasterizer_state = None;
126        let mut sampler_state = None;
127        let mut blend_state = None;
128        unsafe {
129            device.CreateInputLayout(
130                &Self::INPUT_ELEMENTS_DESC,
131                Self::VS_BLOB,
132                Some(&mut input_layout),
133            )?;
134            device.CreateVertexShader(
135                Self::VS_BLOB,
136                None,
137                Some(&mut vertex_shader),
138            )?;
139            device.CreatePixelShader(
140                Self::PS_BLOB,
141                None,
142                Some(&mut pixel_shader),
143            )?;
144            device.CreateRasterizerState(
145                &Self::RASTERIZER_DESC,
146                Some(&mut rasterizer_state),
147            )?;
148            device.CreateSamplerState(
149                &Self::SAMPLER_DESC,
150                Some(&mut sampler_state),
151            )?;
152            device
153                .CreateBlendState(&Self::BLEND_DESC, Some(&mut blend_state))?;
154        };
155        Ok(Self {
156            device: device.clone(),
157            input_layout: input_layout.unwrap(),
158            vertex_shader: vertex_shader.unwrap(),
159            pixel_shader: pixel_shader.unwrap(),
160            rasterizer_state: rasterizer_state.unwrap(),
161            sampler_state: sampler_state.unwrap(),
162            blend_state: blend_state.unwrap(),
163            texture_pool: TexturePool::new(device),
164        })
165    }
166
167    /// Register a user-provided `ID3D11ShaderResourceView` and get a [`egui::TextureId`] for it.
168    ///
169    /// This allows you to use your own DirectX11 textures within egui. The returned
170    /// [`egui::TextureId`] can be used with [`egui::Image`], [`egui::ImageButton`], or
171    /// any other egui widget that accepts a texture ID.
172    ///
173    /// The texture will remain registered until you call [`Renderer::unregister_user_texture`]
174    /// or the [`Renderer`] is dropped.
175    ///
176    /// # Example
177    ///
178    /// ```ignore
179    /// // Assuming you have a ID3D11ShaderResourceView
180    /// let texture_id = renderer.register_user_texture(my_srv);
181    ///
182    /// // Use it in egui
183    /// ui.image(egui::ImageSource::Texture(egui::load::SizedTexture::new(
184    ///     texture_id,
185    ///     egui::vec2(256.0, 256.0),
186    /// )));
187    /// ```
188    pub fn register_user_texture(
189        &mut self,
190        srv: ID3D11ShaderResourceView,
191    ) -> egui::TextureId {
192        self.texture_pool.register_user_texture(srv)
193    }
194
195    /// Unregister a user texture by its [`egui::TextureId`].
196    ///
197    /// Returns `true` if the texture was found and removed, `false` otherwise.
198    /// Note that this only works for user-registered textures, not textures
199    /// managed by egui itself.
200    pub fn unregister_user_texture(&mut self, tid: egui::TextureId) -> bool {
201        self.texture_pool.unregister_user_texture(tid)
202    }
203
204    /// Render the output of `egui` to the provided `render_target`.
205    ///
206    /// As `egui` requires color blending in gamma space, **the provided
207    /// `render_target` MUST be in the gamma color space and viewed as
208    /// non-sRGB-aware** (i.e. do NOT use `_SRGB` format in the texture and
209    /// the view).
210    ///
211    /// If you have to render to a render target in linear color space or
212    /// one that is sRGB-aware, you must create an intermediate render target
213    /// in gamma color space and perform a blit operation afterwards.
214    ///
215    /// The `scale_factor` should be the scale factor of your window and not
216    /// confused with [`egui::Context::zoom_factor`]. If you are using `winit`,
217    /// the `scale_factor` can be aquired using `Window::scale_factor`.
218    ///
219    /// ## Error Handling
220    ///
221    /// If any Direct3D resource creation fails, this function will return an
222    /// error. In this case you may have a incomplete or incorrect rendering
223    /// result. You can create the Direct3D11 device with debug layer
224    /// enabled to find out details on the error.
225    /// If the device has been lost, you should drop the [`Renderer`] and create
226    /// a new one.
227    ///
228    /// ## Pipeline State Management
229    ///
230    /// This function sets up its own Direct3D11 pipeline state for rendering on
231    /// the provided device context. It assumes that the hull shader, domain
232    /// shader and geometry shader stages are not active on the provided device
233    /// context without any further checks. It is all *your* responsibility to
234    /// backup the current pipeline state and restore it afterwards if your
235    /// rendering pipeline depends on it.
236    ///
237    /// Particularly, it overrides:
238    /// + The input layout, vertex buffer, index buffer and primitive topology
239    ///   in the input assembly stage;
240    /// + The current shader in the vertex shader stage;
241    /// + The viewport and rasterizer state in the rasterizer stage;
242    /// + The current shader, shader resource slot 0 and sampler slot 0 in the
243    ///   pixel shader stage;
244    /// + The render target(s) and blend state in the output merger stage;
245    pub fn render(
246        &mut self,
247        device_context: &ID3D11DeviceContext,
248        render_target: &ID3D11RenderTargetView,
249        egui_ctx: &egui::Context,
250        egui_output: RendererOutput,
251    ) -> Result<()> {
252        self.texture_pool
253            .update(device_context, egui_output.textures_delta)?;
254
255        if egui_output.shapes.is_empty() {
256            return Ok(());
257        }
258
259        let frame_size = Self::get_render_target_size(render_target)?;
260        let frame_size_scaled = (
261            frame_size.0 as f32 / egui_output.pixels_per_point,
262            frame_size.1 as f32 / egui_output.pixels_per_point,
263        );
264        let zoom_factor = egui_ctx.zoom_factor();
265
266        self.setup(device_context, render_target, frame_size);
267        let meshes = egui_ctx
268            .tessellate(egui_output.shapes, egui_output.pixels_per_point)
269            .into_iter()
270            .filter_map(
271                |ClippedPrimitive {
272                     primitive,
273                     clip_rect,
274                 }| match primitive {
275                    Primitive::Mesh(mesh) => Some((mesh, clip_rect)),
276                    Primitive::Callback(..) => {
277                        log::warn!("paint callbacks are not yet supported.");
278                        None
279                    },
280                },
281            )
282            .filter_map(|(mesh, clip_rect)| {
283                if mesh.indices.is_empty() {
284                    return None;
285                }
286                if mesh.indices.len() % 3 != 0 {
287                    log::warn!(concat!(
288                        "egui wants to draw a incomplete triangle. ",
289                        "this request will be ignored."
290                    ));
291                    return None;
292                }
293                Some(MeshData {
294                    vtx: mesh
295                        .vertices
296                        .into_iter()
297                        .map(|Vertex { pos, uv, color }| VertexData {
298                            pos: Pos2::new(
299                                pos.x * zoom_factor / frame_size_scaled.0 * 2.0
300                                    - 1.0,
301                                1.0 - pos.y * zoom_factor / frame_size_scaled.1
302                                    * 2.0,
303                            ),
304                            uv,
305                            color: [
306                                color[0] as f32 / 255.0,
307                                color[1] as f32 / 255.0,
308                                color[2] as f32 / 255.0,
309                                color[3] as f32 / 255.0,
310                            ],
311                        })
312                        .collect(),
313                    idx: mesh.indices,
314                    tex: mesh.texture_id,
315                    clip_rect: clip_rect
316                        * egui_output.pixels_per_point
317                        * zoom_factor,
318                })
319            });
320        for mesh in meshes {
321            Self::draw_mesh(
322                &self.device,
323                device_context,
324                &self.texture_pool,
325                mesh,
326            )?;
327        }
328        Ok(())
329    }
330
331    fn setup(
332        &mut self,
333        ctx: &ID3D11DeviceContext,
334        render_target: &ID3D11RenderTargetView,
335        frame_size: (u32, u32),
336    ) {
337        unsafe {
338            ctx.IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
339            ctx.IASetInputLayout(&self.input_layout);
340            ctx.VSSetShader(&self.vertex_shader, None);
341            ctx.PSSetShader(&self.pixel_shader, None);
342            ctx.RSSetState(&self.rasterizer_state);
343            ctx.RSSetViewports(Some(&[D3D11_VIEWPORT {
344                TopLeftX: 0.,
345                TopLeftY: 0.,
346                Width: frame_size.0 as _,
347                Height: frame_size.1 as _,
348                MinDepth: 0.,
349                MaxDepth: 1.,
350            }]));
351            ctx.PSSetSamplers(0, Some(&[Some(self.sampler_state.clone())]));
352            ctx.OMSetRenderTargets(Some(&[Some(render_target.clone())]), None);
353            ctx.OMSetBlendState(&self.blend_state, Some(&[0.; 4]), u32::MAX);
354        }
355    }
356
357    fn draw_mesh(
358        device: &ID3D11Device,
359        device_context: &ID3D11DeviceContext,
360        texture_pool: &TexturePool,
361        mesh: MeshData,
362    ) -> Result<()> {
363        let vb = Self::create_index_buffer(device, &mesh.idx)?;
364        let ib = Self::create_vertex_buffer(device, &mesh.vtx)?;
365        unsafe {
366            device_context.IASetVertexBuffers(
367                0,
368                1,
369                Some(&Some(ib)),
370                Some(&(mem::size_of::<VertexData>() as _)),
371                Some(&0),
372            );
373            device_context.IASetIndexBuffer(&vb, DXGI_FORMAT_R32_UINT, 0);
374            device_context.RSSetScissorRects(Some(&[RECT {
375                left: mesh.clip_rect.left() as _,
376                top: mesh.clip_rect.top() as _,
377                right: mesh.clip_rect.right() as _,
378                bottom: mesh.clip_rect.bottom() as _,
379            }]));
380        }
381        if let Some(srv) = texture_pool.get_srv(mesh.tex) {
382            unsafe {
383                device_context.PSSetShaderResources(0, Some(&[Some(srv)]))
384            };
385        } else {
386            log::warn!(
387                concat!(
388                    "egui wants to sample a non-existing texture {:?}.",
389                    "this request will be ignored."
390                ),
391                mesh.tex
392            );
393        };
394        unsafe { device_context.DrawIndexed(mesh.idx.len() as _, 0, 0) };
395        Ok(())
396    }
397}
398
399impl Renderer {
400    const VS_BLOB: &'static [u8] = include_bytes!("../shaders/vs_egui.bin");
401    const PS_BLOB: &'static [u8] = include_bytes!("../shaders/ps_egui.bin");
402
403    const INPUT_ELEMENTS_DESC: [D3D11_INPUT_ELEMENT_DESC; 3] = [
404        D3D11_INPUT_ELEMENT_DESC {
405            SemanticName: windows::core::s!("POSITION"),
406            SemanticIndex: 0,
407            Format: DXGI_FORMAT_R32G32_FLOAT,
408            InputSlot: 0,
409            AlignedByteOffset: 0,
410            InputSlotClass: D3D11_INPUT_PER_VERTEX_DATA,
411            InstanceDataStepRate: 0,
412        },
413        D3D11_INPUT_ELEMENT_DESC {
414            SemanticName: windows::core::s!("TEXCOORD"),
415            SemanticIndex: 0,
416            Format: DXGI_FORMAT_R32G32_FLOAT,
417            InputSlot: 0,
418            AlignedByteOffset: D3D11_APPEND_ALIGNED_ELEMENT,
419            InputSlotClass: D3D11_INPUT_PER_VERTEX_DATA,
420            InstanceDataStepRate: 0,
421        },
422        D3D11_INPUT_ELEMENT_DESC {
423            SemanticName: windows::core::s!("COLOR"),
424            SemanticIndex: 0,
425            Format: DXGI_FORMAT_R32G32B32A32_FLOAT,
426            InputSlot: 0,
427            AlignedByteOffset: D3D11_APPEND_ALIGNED_ELEMENT,
428            InputSlotClass: D3D11_INPUT_PER_VERTEX_DATA,
429            InstanceDataStepRate: 0,
430        },
431    ];
432
433    const RASTERIZER_DESC: D3D11_RASTERIZER_DESC = D3D11_RASTERIZER_DESC {
434        FillMode: D3D11_FILL_SOLID,
435        CullMode: D3D11_CULL_NONE,
436        FrontCounterClockwise: BOOL(0),
437        DepthBias: 0,
438        DepthBiasClamp: 0.,
439        SlopeScaledDepthBias: 0.,
440        DepthClipEnable: BOOL(0),
441        ScissorEnable: BOOL(1),
442        MultisampleEnable: BOOL(0),
443        AntialiasedLineEnable: BOOL(0),
444    };
445
446    const SAMPLER_DESC: D3D11_SAMPLER_DESC = D3D11_SAMPLER_DESC {
447        Filter: D3D11_FILTER_MIN_MAG_MIP_LINEAR,
448        AddressU: D3D11_TEXTURE_ADDRESS_BORDER,
449        AddressV: D3D11_TEXTURE_ADDRESS_BORDER,
450        AddressW: D3D11_TEXTURE_ADDRESS_BORDER,
451        ComparisonFunc: D3D11_COMPARISON_ALWAYS,
452        BorderColor: [1., 1., 1., 1.],
453        ..zeroed()
454    };
455
456    const BLEND_DESC: D3D11_BLEND_DESC = D3D11_BLEND_DESC {
457        RenderTarget: [
458            D3D11_RENDER_TARGET_BLEND_DESC {
459                BlendEnable: BOOL(1),
460                SrcBlend: D3D11_BLEND_ONE,
461                DestBlend: D3D11_BLEND_INV_SRC_ALPHA,
462                BlendOp: D3D11_BLEND_OP_ADD,
463                SrcBlendAlpha: D3D11_BLEND_INV_DEST_ALPHA,
464                DestBlendAlpha: D3D11_BLEND_ONE,
465                BlendOpAlpha: D3D11_BLEND_OP_ADD,
466                RenderTargetWriteMask: D3D11_COLOR_WRITE_ENABLE_ALL.0 as _,
467            },
468            zeroed(),
469            zeroed(),
470            zeroed(),
471            zeroed(),
472            zeroed(),
473            zeroed(),
474            zeroed(),
475        ],
476        ..zeroed()
477    };
478}
479
480impl Renderer {
481    fn create_vertex_buffer(
482        device: &ID3D11Device,
483        data: &[VertexData],
484    ) -> Result<ID3D11Buffer> {
485        let mut vertex_buffer = None;
486        unsafe {
487            device.CreateBuffer(
488                &D3D11_BUFFER_DESC {
489                    ByteWidth: mem::size_of_val(data) as _,
490                    Usage: D3D11_USAGE_IMMUTABLE,
491                    BindFlags: D3D11_BIND_VERTEX_BUFFER.0 as _,
492                    ..D3D11_BUFFER_DESC::default()
493                },
494                Some(&D3D11_SUBRESOURCE_DATA {
495                    pSysMem: data.as_ptr() as _,
496                    ..D3D11_SUBRESOURCE_DATA::default()
497                }),
498                Some(&mut vertex_buffer),
499            )
500        }?;
501        Ok(vertex_buffer.unwrap())
502    }
503
504    fn create_index_buffer(
505        device: &ID3D11Device,
506        data: &[u32],
507    ) -> Result<ID3D11Buffer> {
508        let mut index_buffer = None;
509        unsafe {
510            device.CreateBuffer(
511                &D3D11_BUFFER_DESC {
512                    ByteWidth: mem::size_of_val(data) as _,
513                    Usage: D3D11_USAGE_IMMUTABLE,
514                    BindFlags: D3D11_BIND_INDEX_BUFFER.0 as _,
515                    ..D3D11_BUFFER_DESC::default()
516                },
517                Some(&D3D11_SUBRESOURCE_DATA {
518                    pSysMem: data.as_ptr() as _,
519                    ..D3D11_SUBRESOURCE_DATA::default()
520                }),
521                Some(&mut index_buffer),
522            )
523        }?;
524        Ok(index_buffer.unwrap())
525    }
526
527    fn get_render_target_size(
528        rtv: &ID3D11RenderTargetView,
529    ) -> Result<(u32, u32)> {
530        let tex = unsafe { rtv.GetResource() }?.cast::<ID3D11Texture2D>()?;
531        let mut desc = zeroed();
532        unsafe { tex.GetDesc(&mut desc) };
533        Ok((desc.Width, desc.Height))
534    }
535}