directx_atlas.rs

  1use collections::FxHashMap;
  2use etagere::BucketedAtlasAllocator;
  3use parking_lot::Mutex;
  4use windows::Win32::Graphics::{
  5    Direct3D11::{
  6        D3D11_BIND_SHADER_RESOURCE, D3D11_BOX, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT,
  7        ID3D11Device, ID3D11DeviceContext, ID3D11ShaderResourceView, ID3D11Texture2D,
  8    },
  9    Dxgi::Common::*,
 10};
 11
 12use crate::{
 13    AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, DevicePixels, PlatformAtlas,
 14    Point, Size, platform::AtlasTextureList,
 15};
 16
 17pub(crate) struct DirectXAtlas(Mutex<DirectXAtlasState>);
 18
 19struct DirectXAtlasState {
 20    device: ID3D11Device,
 21    device_context: ID3D11DeviceContext,
 22    monochrome_textures: AtlasTextureList<DirectXAtlasTexture>,
 23    polychrome_textures: AtlasTextureList<DirectXAtlasTexture>,
 24    tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
 25}
 26
 27struct DirectXAtlasTexture {
 28    id: AtlasTextureId,
 29    bytes_per_pixel: u32,
 30    allocator: BucketedAtlasAllocator,
 31    texture: ID3D11Texture2D,
 32    view: [Option<ID3D11ShaderResourceView>; 1],
 33    live_atlas_keys: u32,
 34}
 35
 36impl DirectXAtlas {
 37    pub(crate) fn new(device: &ID3D11Device, device_context: &ID3D11DeviceContext) -> Self {
 38        DirectXAtlas(Mutex::new(DirectXAtlasState {
 39            device: device.clone(),
 40            device_context: device_context.clone(),
 41            monochrome_textures: Default::default(),
 42            polychrome_textures: Default::default(),
 43            tiles_by_key: Default::default(),
 44        }))
 45    }
 46
 47    pub(crate) fn get_texture_view(
 48        &self,
 49        id: AtlasTextureId,
 50    ) -> [Option<ID3D11ShaderResourceView>; 1] {
 51        let lock = self.0.lock();
 52        let tex = lock.texture(id);
 53        tex.view.clone()
 54    }
 55
 56    pub(crate) fn handle_device_lost(
 57        &self,
 58        device: &ID3D11Device,
 59        device_context: &ID3D11DeviceContext,
 60    ) {
 61        let mut lock = self.0.lock();
 62        lock.device = device.clone();
 63        lock.device_context = device_context.clone();
 64        lock.monochrome_textures = AtlasTextureList::default();
 65        lock.polychrome_textures = AtlasTextureList::default();
 66        lock.tiles_by_key.clear();
 67    }
 68}
 69
 70impl PlatformAtlas for DirectXAtlas {
 71    fn get_or_insert_with<'a>(
 72        &self,
 73        key: &AtlasKey,
 74        build: &mut dyn FnMut() -> anyhow::Result<
 75            Option<(Size<DevicePixels>, std::borrow::Cow<'a, [u8]>)>,
 76        >,
 77    ) -> anyhow::Result<Option<AtlasTile>> {
 78        let mut lock = self.0.lock();
 79        if let Some(tile) = lock.tiles_by_key.get(key) {
 80            Ok(Some(tile.clone()))
 81        } else {
 82            let Some((size, bytes)) = build()? else {
 83                return Ok(None);
 84            };
 85            let tile = lock
 86                .allocate(size, key.texture_kind())
 87                .ok_or_else(|| anyhow::anyhow!("failed to allocate"))?;
 88            let texture = lock.texture(tile.texture_id);
 89            texture.upload(&lock.device_context, tile.bounds, &bytes);
 90            lock.tiles_by_key.insert(key.clone(), tile.clone());
 91            Ok(Some(tile))
 92        }
 93    }
 94
 95    fn remove(&self, key: &AtlasKey) {
 96        let mut lock = self.0.lock();
 97
 98        let Some(id) = lock.tiles_by_key.remove(key).map(|tile| tile.texture_id) else {
 99            return;
100        };
101
102        let textures = match id.kind {
103            AtlasTextureKind::Monochrome => &mut lock.monochrome_textures,
104            AtlasTextureKind::Polychrome => &mut lock.polychrome_textures,
105        };
106
107        let Some(texture_slot) = textures.textures.get_mut(id.index as usize) else {
108            return;
109        };
110
111        if let Some(mut texture) = texture_slot.take() {
112            texture.decrement_ref_count();
113            if texture.is_unreferenced() {
114                textures.free_list.push(texture.id.index as usize);
115                lock.tiles_by_key.remove(key);
116            } else {
117                *texture_slot = Some(texture);
118            }
119        }
120    }
121}
122
123impl DirectXAtlasState {
124    fn allocate(
125        &mut self,
126        size: Size<DevicePixels>,
127        texture_kind: AtlasTextureKind,
128    ) -> Option<AtlasTile> {
129        {
130            let textures = match texture_kind {
131                AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
132                AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
133            };
134
135            if let Some(tile) = textures
136                .iter_mut()
137                .rev()
138                .find_map(|texture| texture.allocate(size))
139            {
140                return Some(tile);
141            }
142        }
143
144        let texture = self.push_texture(size, texture_kind)?;
145        texture.allocate(size)
146    }
147
148    fn push_texture(
149        &mut self,
150        min_size: Size<DevicePixels>,
151        kind: AtlasTextureKind,
152    ) -> Option<&mut DirectXAtlasTexture> {
153        const DEFAULT_ATLAS_SIZE: Size<DevicePixels> = Size {
154            width: DevicePixels(1024),
155            height: DevicePixels(1024),
156        };
157        // Max texture size for DirectX. See:
158        // https://learn.microsoft.com/en-us/windows/win32/direct3d11/overviews-direct3d-11-resources-limits
159        const MAX_ATLAS_SIZE: Size<DevicePixels> = Size {
160            width: DevicePixels(16384),
161            height: DevicePixels(16384),
162        };
163        let size = min_size.min(&MAX_ATLAS_SIZE).max(&DEFAULT_ATLAS_SIZE);
164        let pixel_format;
165        let bind_flag;
166        let bytes_per_pixel;
167        match kind {
168            AtlasTextureKind::Monochrome => {
169                pixel_format = DXGI_FORMAT_R8_UNORM;
170                bind_flag = D3D11_BIND_SHADER_RESOURCE;
171                bytes_per_pixel = 1;
172            }
173            AtlasTextureKind::Polychrome => {
174                pixel_format = DXGI_FORMAT_B8G8R8A8_UNORM;
175                bind_flag = D3D11_BIND_SHADER_RESOURCE;
176                bytes_per_pixel = 4;
177            }
178        }
179        let texture_desc = D3D11_TEXTURE2D_DESC {
180            Width: size.width.0 as u32,
181            Height: size.height.0 as u32,
182            MipLevels: 1,
183            ArraySize: 1,
184            Format: pixel_format,
185            SampleDesc: DXGI_SAMPLE_DESC {
186                Count: 1,
187                Quality: 0,
188            },
189            Usage: D3D11_USAGE_DEFAULT,
190            BindFlags: bind_flag.0 as u32,
191            CPUAccessFlags: 0,
192            MiscFlags: 0,
193        };
194        let mut texture: Option<ID3D11Texture2D> = None;
195        unsafe {
196            // This only returns None if the device is lost, which we will recreate later.
197            // So it's ok to return None here.
198            self.device
199                .CreateTexture2D(&texture_desc, None, Some(&mut texture))
200                .ok()?;
201        }
202        let texture = texture.unwrap();
203
204        let texture_list = match kind {
205            AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
206            AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
207        };
208        let index = texture_list.free_list.pop();
209        let view = unsafe {
210            let mut view = None;
211            self.device
212                .CreateShaderResourceView(&texture, None, Some(&mut view))
213                .ok()?;
214            [view]
215        };
216        let atlas_texture = DirectXAtlasTexture {
217            id: AtlasTextureId {
218                index: index.unwrap_or(texture_list.textures.len()) as u32,
219                kind,
220            },
221            bytes_per_pixel,
222            allocator: etagere::BucketedAtlasAllocator::new(size.into()),
223            texture,
224            view,
225            live_atlas_keys: 0,
226        };
227        if let Some(ix) = index {
228            texture_list.textures[ix] = Some(atlas_texture);
229            texture_list.textures.get_mut(ix).unwrap().as_mut()
230        } else {
231            texture_list.textures.push(Some(atlas_texture));
232            texture_list.textures.last_mut().unwrap().as_mut()
233        }
234    }
235
236    fn texture(&self, id: AtlasTextureId) -> &DirectXAtlasTexture {
237        match id.kind {
238            crate::AtlasTextureKind::Monochrome => &self.monochrome_textures[id.index as usize]
239                .as_ref()
240                .unwrap(),
241            crate::AtlasTextureKind::Polychrome => &self.polychrome_textures[id.index as usize]
242                .as_ref()
243                .unwrap(),
244        }
245    }
246}
247
248impl DirectXAtlasTexture {
249    fn allocate(&mut self, size: Size<DevicePixels>) -> Option<AtlasTile> {
250        let allocation = self.allocator.allocate(size.into())?;
251        let tile = AtlasTile {
252            texture_id: self.id,
253            tile_id: allocation.id.into(),
254            bounds: Bounds {
255                origin: allocation.rectangle.min.into(),
256                size,
257            },
258            padding: 0,
259        };
260        self.live_atlas_keys += 1;
261        Some(tile)
262    }
263
264    fn upload(
265        &self,
266        device_context: &ID3D11DeviceContext,
267        bounds: Bounds<DevicePixels>,
268        bytes: &[u8],
269    ) {
270        unsafe {
271            device_context.UpdateSubresource(
272                &self.texture,
273                0,
274                Some(&D3D11_BOX {
275                    left: bounds.left().0 as u32,
276                    top: bounds.top().0 as u32,
277                    front: 0,
278                    right: bounds.right().0 as u32,
279                    bottom: bounds.bottom().0 as u32,
280                    back: 1,
281                }),
282                bytes.as_ptr() as _,
283                bounds.size.width.to_bytes(self.bytes_per_pixel as u8),
284                0,
285            );
286        }
287    }
288
289    fn decrement_ref_count(&mut self) {
290        self.live_atlas_keys -= 1;
291    }
292
293    fn is_unreferenced(&mut self) -> bool {
294        self.live_atlas_keys == 0
295    }
296}
297
298impl From<Size<DevicePixels>> for etagere::Size {
299    fn from(size: Size<DevicePixels>) -> Self {
300        etagere::Size::new(size.width.into(), size.height.into())
301    }
302}
303
304impl From<etagere::Point> for Point<DevicePixels> {
305    fn from(value: etagere::Point) -> Self {
306        Point {
307            x: DevicePixels::from(value.x),
308            y: DevicePixels::from(value.y),
309        }
310    }
311}