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