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
59pub 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 pub const fn new() -> Self {
104 Self {
105 data: Mutex::new(None),
106 hwnd: OnceCell::new(),
107 }
108 }
109
110 pub fn is_ready(&self) -> bool {
113 self.hwnd.get().is_some()
114 }
115
116 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 #[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 #[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 #[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 #[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 (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 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 #[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}