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    Point, 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 handle_device_lost(
 58        &self,
 59        device: ID3D11Device,
 60        device_context: ID3D11DeviceContext,
 61    ) -> anyhow::Result<()> {
 62        let mut lock = self.0.lock();
 63        lock.device = device;
 64        lock.device_context = device_context;
 65        lock.monochrome_textures = AtlasTextureList::default();
 66        lock.polychrome_textures = AtlasTextureList::default();
 67        lock.tiles_by_key.clear();
 68        Ok(())
 69    }
 70}
 71
 72impl PlatformAtlas for DirectXAtlas {
 73    fn get_or_insert_with<'a>(
 74        &self,
 75        key: &AtlasKey,
 76        build: &mut dyn FnMut() -> anyhow::Result<
 77            Option<(Size<DevicePixels>, std::borrow::Cow<'a, [u8]>)>,
 78        >,
 79    ) -> anyhow::Result<Option<AtlasTile>> {
 80        let mut lock = self.0.lock();
 81        if let Some(tile) = lock.tiles_by_key.get(key) {
 82            Ok(Some(tile.clone()))
 83        } else {
 84            let Some((size, bytes)) = build()? else {
 85                return Ok(None);
 86            };
 87            let tile = lock
 88                .allocate(size, key.texture_kind())
 89                .ok_or_else(|| anyhow::anyhow!("failed to allocate"))?;
 90            let texture = lock.texture(tile.texture_id);
 91            texture.upload(&lock.device_context, tile.bounds, &bytes);
 92            lock.tiles_by_key.insert(key.clone(), tile.clone());
 93            Ok(Some(tile))
 94        }
 95    }
 96
 97    fn remove(&self, key: &AtlasKey) {
 98        let mut lock = self.0.lock();
 99
100        let Some(id) = lock.tiles_by_key.remove(key).map(|tile| tile.texture_id) else {
101            return;
102        };
103
104        let textures = match id.kind {
105            AtlasTextureKind::Monochrome => &mut lock.monochrome_textures,
106            AtlasTextureKind::Polychrome => &mut lock.polychrome_textures,
107        };
108
109        let Some(texture_slot) = textures.textures.get_mut(id.index as usize) else {
110            return;
111        };
112
113        if let Some(mut texture) = texture_slot.take() {
114            texture.decrement_ref_count();
115            if texture.is_unreferenced() {
116                textures.free_list.push(texture.id.index as usize);
117                lock.tiles_by_key.remove(key);
118            } else {
119                *texture_slot = Some(texture);
120            }
121        }
122    }
123}
124
125impl DirectXAtlasState {
126    fn allocate(
127        &mut self,
128        size: Size<DevicePixels>,
129        texture_kind: AtlasTextureKind,
130    ) -> Option<AtlasTile> {
131        {
132            let textures = match texture_kind {
133                AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
134                AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
135            };
136
137            if let Some(tile) = textures
138                .iter_mut()
139                .rev()
140                .find_map(|texture| texture.allocate(size))
141            {
142                return Some(tile);
143            }
144        }
145
146        let texture = self.push_texture(size, texture_kind);
147        texture.allocate(size)
148    }
149
150    fn push_texture(
151        &mut self,
152        min_size: Size<DevicePixels>,
153        kind: AtlasTextureKind,
154    ) -> &mut DirectXAtlasTexture {
155        const DEFAULT_ATLAS_SIZE: Size<DevicePixels> = Size {
156            width: DevicePixels(1024),
157            height: DevicePixels(1024),
158        };
159        // Max texture size for DirectX. See:
160        // https://learn.microsoft.com/en-us/windows/win32/direct3d11/overviews-direct3d-11-resources-limits
161        const MAX_ATLAS_SIZE: Size<DevicePixels> = Size {
162            width: DevicePixels(16384),
163            height: DevicePixels(16384),
164        };
165        let size = min_size.min(&MAX_ATLAS_SIZE).max(&DEFAULT_ATLAS_SIZE);
166        let pixel_format;
167        let bind_flag;
168        let bytes_per_pixel;
169        match kind {
170            AtlasTextureKind::Monochrome => {
171                pixel_format = DXGI_FORMAT_A8_UNORM;
172                bind_flag = D3D11_BIND_SHADER_RESOURCE;
173                bytes_per_pixel = 1;
174            }
175            AtlasTextureKind::Polychrome => {
176                pixel_format = DXGI_FORMAT_B8G8R8A8_UNORM;
177                bind_flag = D3D11_BIND_SHADER_RESOURCE;
178                bytes_per_pixel = 4;
179            }
180        }
181        let texture_desc = D3D11_TEXTURE2D_DESC {
182            Width: size.width.0 as u32,
183            Height: size.height.0 as u32,
184            MipLevels: 1,
185            ArraySize: 1,
186            Format: pixel_format,
187            SampleDesc: DXGI_SAMPLE_DESC {
188                Count: 1,
189                Quality: 0,
190            },
191            Usage: D3D11_USAGE_DEFAULT,
192            BindFlags: bind_flag.0 as u32,
193            CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32,
194            MiscFlags: 0,
195        };
196        let mut texture: Option<ID3D11Texture2D> = None;
197        unsafe {
198            self.device
199                .CreateTexture2D(&texture_desc, None, Some(&mut texture))
200                .unwrap();
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                .unwrap();
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().unwrap()
230        } else {
231            texture_list.textures.push(Some(atlas_texture));
232            texture_list.textures.last_mut().unwrap().as_mut().unwrap()
233        }
234    }
235
236    fn texture(&self, id: AtlasTextureId) -> &DirectXAtlasTexture {
237        let textures = match id.kind {
238            crate::AtlasTextureKind::Monochrome => &self.monochrome_textures,
239            crate::AtlasTextureKind::Polychrome => &self.polychrome_textures,
240            // crate::AtlasTextureKind::Path => &self.path_textures,
241        };
242        textures[id.index as usize].as_ref().unwrap()
243    }
244}
245
246impl DirectXAtlasTexture {
247    fn allocate(&mut self, size: Size<DevicePixels>) -> Option<AtlasTile> {
248        let allocation = self.allocator.allocate(size.into())?;
249        let tile = AtlasTile {
250            texture_id: self.id,
251            tile_id: allocation.id.into(),
252            bounds: Bounds {
253                origin: allocation.rectangle.min.into(),
254                size,
255            },
256            padding: 0,
257        };
258        self.live_atlas_keys += 1;
259        Some(tile)
260    }
261
262    fn upload(
263        &self,
264        device_context: &ID3D11DeviceContext,
265        bounds: Bounds<DevicePixels>,
266        bytes: &[u8],
267    ) {
268        unsafe {
269            device_context.UpdateSubresource(
270                &self.texture,
271                0,
272                Some(&D3D11_BOX {
273                    left: bounds.left().0 as u32,
274                    top: bounds.top().0 as u32,
275                    front: 0,
276                    right: bounds.right().0 as u32,
277                    bottom: bounds.bottom().0 as u32,
278                    back: 1,
279                }),
280                bytes.as_ptr() as _,
281                bounds.size.width.to_bytes(self.bytes_per_pixel as u8),
282                0,
283            );
284        }
285    }
286
287    fn decrement_ref_count(&mut self) {
288        self.live_atlas_keys -= 1;
289    }
290
291    fn is_unreferenced(&mut self) -> bool {
292        self.live_atlas_keys == 0
293    }
294}
295
296impl From<Size<DevicePixels>> for etagere::Size {
297    fn from(size: Size<DevicePixels>) -> Self {
298        etagere::Size::new(size.width.into(), size.height.into())
299    }
300}
301
302impl From<etagere::Point> for Point<DevicePixels> {
303    fn from(value: etagere::Point) -> Self {
304        Point {
305            x: DevicePixels::from(value.x),
306            y: DevicePixels::from(value.y),
307        }
308    }
309}